-
Notifications
You must be signed in to change notification settings - Fork 0
Custom Inputs #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Custom Inputs #36
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 511dc49
chore: update package dependencies and configuration
jaruesink c6be185
feat: enhance radio group and checkbox components with custom impleme…
jaruesink bf932a1
feat: enhance radio group and checkbox components with new props and …
jaruesink 5e766e1
feat: enhance Switch component with customizable components and impro…
jaruesink b67816c
feat: enhance textarea and text field components with customizable st…
jaruesink b184404
feat: enhance text field stories with forwardRef support and improved…
jaruesink 8d57a6c
feat: refine IconInput component in text field stories for improved a…
jaruesink 9573a46
Add comprehensive documentation to checkbox-custom.stories.tsx
6167a4f
Add prefix and suffix props to TextField component
codegen-sh[bot] 07f423c
feat: add measurement field to text field stories and enhance TextFie…
jaruesink 6329095
Merge pull request #41 from lambda-curry/feature/mkt-159-text-field-p…
jaruesink 4060182
refactor: improve structure of TextField component by consolidating F…
jaruesink cbff156
Merge branch 'feature/mkt-159-text-field-prefix-suffix' into custom-i…
jaruesink 72adb4c
chore: downgrade React and TypeScript dependencies to version 18
jaruesink 9b9ef7d
Merge branch 'main' of github.com:lambda-curry/forms into custom-inputs
jaruesink File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
373 changes: 373 additions & 0 deletions
373
apps/docs/src/remix-hook-form/checkbox-custom.stories.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| } | ||
| }, | ||
| }; | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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.