Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
6410659
feat: enhance checkbox components with custom label support and impro…
jaruesink Mar 15, 2025
511dc49
chore: update package dependencies and configuration
jaruesink Mar 15, 2025
c6be185
feat: enhance radio group and checkbox components with custom impleme…
jaruesink Mar 15, 2025
bf932a1
feat: enhance radio group and checkbox components with new props and …
jaruesink Mar 15, 2025
5e766e1
feat: enhance Switch component with customizable components and impro…
jaruesink Mar 15, 2025
b67816c
feat: enhance textarea and text field components with customizable st…
jaruesink Mar 15, 2025
b184404
feat: enhance text field stories with forwardRef support and improved…
jaruesink Mar 15, 2025
8d57a6c
feat: refine IconInput component in text field stories for improved a…
jaruesink Mar 15, 2025
9573a46
Add comprehensive documentation to checkbox-custom.stories.tsx
Mar 18, 2025
6167a4f
Add prefix and suffix props to TextField component
codegen-sh[bot] Mar 21, 2025
07f423c
feat: add measurement field to text field stories and enhance TextFie…
jaruesink Mar 21, 2025
6329095
Merge pull request #41 from lambda-curry/feature/mkt-159-text-field-p…
jaruesink Mar 21, 2025
4060182
refactor: improve structure of TextField component by consolidating F…
jaruesink Mar 21, 2025
cbff156
Merge branch 'feature/mkt-159-text-field-prefix-suffix' into custom-i…
jaruesink Mar 21, 2025
72adb4c
chore: downgrade React and TypeScript dependencies to version 18
jaruesink Mar 21, 2025
9b9ef7d
Merge branch 'main' of github.com:lambda-curry/forms into custom-inputs
jaruesink Mar 21, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
935 changes: 935 additions & 0 deletions ai/CustomInputsProject.md

Large diffs are not rendered by default.

373 changes: 373 additions & 0 deletions apps/docs/src/remix-hook-form/checkbox-custom.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,373 @@
import { zodResolver } from '@hookform/resolvers/zod';
import { Checkbox } from '@lambdacurry/forms/remix-hook-form/checkbox';
import type { FormLabel, FormMessage } from '@lambdacurry/forms/remix-hook-form/form';
import { Button } from '@lambdacurry/forms/ui/button';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import type { ActionFunctionArgs } from '@remix-run/node';
import { useFetcher } from '@remix-run/react';
import type { Meta, StoryObj } from '@storybook/react';
import { expect, userEvent, within } from '@storybook/test';
import * as React from 'react';
import { RemixFormProvider, getValidatedFormData, useRemixForm } from 'remix-hook-form';
import { z } from 'zod';
import { withRemixStubDecorator } from '../lib/storybook/remix-stub';

const formSchema = z.object({
terms: z.boolean().refine((val) => val === true, 'You must accept the terms and conditions'),
marketing: z.boolean().optional(),
required: z.boolean().refine((val) => val === true, 'This field is required'),
});

type FormData = z.infer<typeof formSchema>;

// Custom checkbox component
const PurpleCheckbox = React.forwardRef<
HTMLButtonElement,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>((props, ref) => (
<CheckboxPrimitive.Root
ref={ref}
{...props}
className="h-8 w-8 rounded-full border-4 border-purple-500 bg-white data-[state=checked]:bg-purple-500"
>
{props.children}
</CheckboxPrimitive.Root>
));
PurpleCheckbox.displayName = 'PurpleCheckbox';

// Custom indicator
const PurpleIndicator = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Indicator>
>((props, ref) => (
<CheckboxPrimitive.Indicator
ref={ref}
{...props}
className="flex h-full w-full items-center justify-center text-white"
>
</CheckboxPrimitive.Indicator>
));
PurpleIndicator.displayName = 'PurpleIndicator';

// Custom form label component
const CustomLabel = React.forwardRef<HTMLLabelElement, React.ComponentPropsWithoutRef<typeof FormLabel>>(
({ className, htmlFor, ...props }, ref) => (
<label
ref={ref}
htmlFor={htmlFor}
className={`custom-label text-purple-600 font-bold text-lg ${className}`}
{...props}
>
{props.children} ★
</label>
),
);
CustomLabel.displayName = 'CustomLabel';

// Custom error message component
const CustomErrorMessage = React.forwardRef<HTMLParagraphElement, React.ComponentPropsWithoutRef<typeof FormMessage>>(
({ className, ...props }, ref) => (
<p
ref={ref}
className={`custom-error flex items-center text-red-500 bg-red-100 p-2 rounded-md ${className}`}
{...props}
>
<span className="mr-1 text-lg">⚠️</span> {props.children}
</p>
),
);
CustomErrorMessage.displayName = 'CustomErrorMessage';

// Example with custom checkbox components
const PurpleCheckboxExample = () => {
const fetcher = useFetcher<{ message: string }>();
const methods = useRemixForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
terms: false as true,
marketing: false,
required: false as true,
},
fetcher,
submitConfig: {
action: '/',
method: 'post',
},
});

return (
<RemixFormProvider {...methods}>
<fetcher.Form onSubmit={methods.handleSubmit}>
<div className="grid gap-8">
<Checkbox
name="terms"
label="Accept terms and conditions"
description="You must accept our terms to continue"
components={{
Checkbox: PurpleCheckbox,
CheckboxIndicator: PurpleIndicator,
}}
/>
</div>
<Button type="submit" className="mt-8">
Submit
</Button>
{fetcher.data?.message && <p className="mt-2 text-green-600">{fetcher.data.message}</p>}
</fetcher.Form>
</RemixFormProvider>
);
};

// Example with custom label components
const CustomLabelExample = () => {
const fetcher = useFetcher<{ message: string }>();
const methods = useRemixForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
terms: false as true,
marketing: false,
required: false as true,
},
fetcher,
submitConfig: {
action: '/',
method: 'post',
},
});

return (
<RemixFormProvider {...methods}>
<fetcher.Form onSubmit={methods.handleSubmit}>
<div className="grid gap-8">
<Checkbox
name="required"
label="This is a required checkbox"
components={{
FormLabel: CustomLabel,
FormMessage: CustomErrorMessage,
}}
/>
</div>
<Button type="submit" className="mt-8">
Submit
</Button>
{fetcher.data?.message && <p className="mt-2 text-green-600">{fetcher.data.message}</p>}
</fetcher.Form>
</RemixFormProvider>
);
};

// Example with all custom components
const AllCustomComponentsExample = () => {
const fetcher = useFetcher<{ message: string }>();
const methods = useRemixForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
terms: false as true,
marketing: false,
required: false as true,
},
fetcher,
submitConfig: {
action: '/',
method: 'post',
},
});

const customCheckboxComponents = {
Checkbox: PurpleCheckbox,
CheckboxIndicator: PurpleIndicator,
};

const customLabelComponents = {
FormLabel: CustomLabel,
FormMessage: CustomErrorMessage,
};

return (
<RemixFormProvider {...methods}>
<fetcher.Form onSubmit={methods.handleSubmit}>
<div className="grid gap-8">
<Checkbox
name="terms"
label="Accept terms and conditions"
components={{
...customCheckboxComponents,
...customLabelComponents,
}}
/>
<Checkbox
name="marketing"
label="Receive marketing emails"
description="We will send you hourly updates about our products"
// Using default components for this checkbox
/>
<Checkbox name="required" label="This is a required checkbox" components={customLabelComponents} />
</div>
<Button type="submit" className="mt-8">
Submit
</Button>
{fetcher.data?.message && <p className="mt-2 text-green-600">{fetcher.data.message}</p>}
</fetcher.Form>
</RemixFormProvider>
);
};

const handleFormSubmission = async (request: Request) => {
const { errors } = await getValidatedFormData<FormData>(request, zodResolver(formSchema));

if (errors) {
return { errors };
}

return { message: 'Form submitted successfully' };
};

const meta: Meta<typeof Checkbox> = {
title: 'RemixHookForm/Checkbox Customized',
component: Checkbox,
parameters: { layout: 'centered' },
tags: ['autodocs'],
} satisfies Meta<typeof Checkbox>;

export default meta;
type Story = StoryObj<typeof meta>;

export const CustomCheckboxComponentExamples: Story = {
name: 'Custom Checkbox Component Examples',
decorators: [
withRemixStubDecorator({
root: {
Component: AllCustomComponentsExample,
action: async ({ request }: ActionFunctionArgs) => handleFormSubmission(request),
},
}),
],
parameters: {
docs: {
description: {
story: 'Examples of custom checkbox components with different styling options.',
},
source: {
code: `
// Custom checkbox component
const PurpleCheckbox = React.forwardRef<
HTMLButtonElement,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>((props, ref) => (
<CheckboxPrimitive.Root
ref={ref}
{...props}
className="h-8 w-8 rounded-full border-4 border-purple-500 bg-white data-[state=checked]:bg-purple-500"
>
{props.children}
</CheckboxPrimitive.Root>
));

// Custom indicator
const PurpleIndicator = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Indicator>
>((props, ref) => (
<CheckboxPrimitive.Indicator
ref={ref}
{...props}
className="flex h-full w-full items-center justify-center text-white"
>
</CheckboxPrimitive.Indicator>
));

// Custom form label component
const CustomLabel = React.forwardRef<
HTMLLabelElement,
React.ComponentPropsWithoutRef<typeof FormLabel>
>(({ className, htmlFor, ...props }, ref) => (
<label
ref={ref}
htmlFor={htmlFor}
className={\`custom-label text-purple-600 font-bold text-lg \${className}\`}
{...props}
>
{props.children} ★
</label>
));

// Custom error message component
const CustomErrorMessage = React.forwardRef<
HTMLParagraphElement,
React.ComponentPropsWithoutRef<typeof FormMessage>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={\`custom-error flex items-center text-red-500 bg-red-100 p-2 rounded-md \${className}\`}
{...props}
>
<span className="mr-1 text-lg">⚠️</span> {props.children}
</p>
));

// Usage in form
<Checkbox
name="terms"
label="Accept terms and conditions"
components={{
...customCheckboxComponents,
...customLabelComponents,
}}
/>

<Checkbox
name="required"
label="This is a required checkbox"
components={customLabelComponents}
/>`,
},
},
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

// Find all checkboxes
const checkboxElements = canvas.getAllByRole('checkbox', { hidden: true });

// Get all button checkboxes
const checkboxButtons = Array.from(checkboxElements)
.map((checkbox) => checkbox.closest('button'))
.filter((button) => button !== null) as HTMLButtonElement[];

// We should have at least one custom checkbox button
expect(checkboxButtons.length).toBeGreaterThan(0);

// Find the custom purple checkbox (the one with rounded-full class)
const purpleCheckbox = checkboxButtons.find(
(button) => button.classList.contains('rounded-full') && button.classList.contains('border-purple-500'),
);

if (purpleCheckbox) {
// Verify custom checkbox styling
expect(purpleCheckbox).toHaveClass('rounded-full');
expect(purpleCheckbox).toHaveClass('border-purple-500');

// Check the terms checkbox
await userEvent.click(purpleCheckbox);
expect(purpleCheckbox).toHaveAttribute('data-state', 'checked');

// Find the required checkbox (we'll just check all remaining checkboxes)
for (const button of checkboxButtons) {
if (button !== purpleCheckbox) {
await userEvent.click(button);
}
}

// Submit the form
const submitButton = canvas.getByRole('button', { name: 'Submit' });
await userEvent.click(submitButton);

// Verify successful submission
const successMessage = await canvas.findByText('Form submitted successfully');
expect(successMessage).toBeInTheDocument();
}
},
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Inclusion of interactive tests is excellent.

This ensures that custom styling and modifications are validated. Consider adding negative test cases for un-checked required boxes, verifying that error messages display.

Loading