-
Notifications
You must be signed in to change notification settings - Fork 0
Issue 53 add form react hook form component #63
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
Conversation
|
Demo with more test form_demo_more.mp4 |
client/package.json
Outdated
| "tailwind-merge": "^3.3.1", | ||
| "tailwindcss-animate": "^1.0.7" | ||
| "tailwindcss-animate": "^1.0.7", | ||
| "yup": "^1.7.1" |
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.
sorry we'll be using zod for the schema validation instead of yup because it's more suitable for typescript
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.
Pull request overview
This PR introduces a reusable Form component using react-hook-form and zod for validation, addressing issue #53. The implementation provides a declarative API with validation support and flexible error handling options.
- Adds Form and ControlledField components with zod schema validation
- Includes comprehensive documentation and three example implementations
- Adds required dependencies: @hookform/resolvers, react-hook-form, and zod
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 7 comments.
| File | Description |
|---|---|
| client/src/components/ui/form.tsx | New Form component with react-hook-form integration, ControlledField wrapper, and error handling utilities |
| client/src/app/test/form/page.tsx | Test page demonstrating three error handling approaches with example form schemas and input fields |
| client/package.json | Added dependencies for form validation: @hookform/resolvers, react-hook-form, and zod |
| client/package-lock.json | Lock file updates for new form-related dependencies |
Files not reviewed (1)
- client/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
client/src/components/ui/form.tsx
Outdated
| @@ -0,0 +1,150 @@ | |||
| // Usage of Form: | |||
| // import { Form, ControlledField } from "@/components/ui/form"; // Note: both are necessary and not default export! | |||
| // <Form schema={zodSchema} // Note: according to the current implimentation, numbers are also string. For an example please refer to /app/test/form/page.tsx | |||
Copilot
AI
Dec 9, 2025
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.
Typo in comment: "implimentation" should be "implementation".
| // <Form schema={zodSchema} // Note: according to the current implimentation, numbers are also string. For an example please refer to /app/test/form/page.tsx | |
| // <Form schema={zodSchema} // Note: according to the current implementation, numbers are also string. For an example please refer to /app/test/form/page.tsx |
client/src/components/ui/form.tsx
Outdated
| // import { Form, ControlledField } from "@/components/ui/form"; // Note: both are necessary and not default export! | ||
| // <Form schema={zodSchema} // Note: according to the current implimentation, numbers are also string. For an example please refer to /app/test/form/page.tsx | ||
| // onSubmit={submitHandler} // callback function in the form of (data) => handleSumit(data) | ||
| // orError={errorHandler} // optional, allows flexibility of error message display, |
Copilot
AI
Dec 9, 2025
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.
Typo in comment: "orError" should be "onError" to match the actual prop name.
| // orError={errorHandler} // optional, allows flexibility of error message display, | |
| // onError={errorHandler} // optional, allows flexibility of error message display, |
client/src/components/ui/form.tsx
Outdated
| }; | ||
|
|
||
| // To make input validation effective, an extra wrapper is needed for the input field | ||
| type controlledFieldProps<TSchema> = { |
Copilot
AI
Dec 9, 2025
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.
Type name controlledFieldProps does not follow TypeScript naming conventions. Type names should use PascalCase. Consider renaming to ControlledFieldProps.
client/src/components/ui/form.tsx
Outdated
| <> | ||
| <FormProvider {...methods}> | ||
| <form | ||
| onSubmit={methods.handleSubmit(onSubmit, (errors) => { | ||
| if (onError) onError(errors); | ||
| else alert(getErrorMessage(errors)); | ||
| })} | ||
| className={cn( | ||
| "flex w-full flex-col space-y-3 rounded-md border border-[hsl(var(--border))] bg-background px-8 py-8", | ||
| className, | ||
| )} | ||
| > | ||
| {typeof children === "function" ? children(methods) : children} | ||
| </form> | ||
| </FormProvider> | ||
| </> |
Copilot
AI
Dec 9, 2025
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.
The empty fragment wrapper (<></>) around FormProvider serves no purpose and adds unnecessary nesting. Consider removing it to simplify the component structure.
| <> | |
| <FormProvider {...methods}> | |
| <form | |
| onSubmit={methods.handleSubmit(onSubmit, (errors) => { | |
| if (onError) onError(errors); | |
| else alert(getErrorMessage(errors)); | |
| })} | |
| className={cn( | |
| "flex w-full flex-col space-y-3 rounded-md border border-[hsl(var(--border))] bg-background px-8 py-8", | |
| className, | |
| )} | |
| > | |
| {typeof children === "function" ? children(methods) : children} | |
| </form> | |
| </FormProvider> | |
| </> | |
| <FormProvider {...methods}> | |
| <form | |
| onSubmit={methods.handleSubmit(onSubmit, (errors) => { | |
| if (onError) onError(errors); | |
| else alert(getErrorMessage(errors)); | |
| })} | |
| className={cn( | |
| "flex w-full flex-col space-y-3 rounded-md border border-[hsl(var(--border))] bg-background px-8 py-8", | |
| className, | |
| )} | |
| > | |
| {typeof children === "function" ? children(methods) : children} | |
| </form> | |
| </FormProvider> |
client/src/app/test/form/page.tsx
Outdated
| const schema = z.object({ | ||
| firstname: z.string().min(1, "Firstname is required"), | ||
| lastname: z.string().min(1, "Lastname is required"), | ||
| middlename: z.string().optional(), | ||
| age: z | ||
| .string() | ||
| .min(1, "Age is required") | ||
| .regex(/^\d+$/, { message: "Age must be a non-negative integer" }) | ||
| .refine( | ||
| (val) => { | ||
| const num = Number(val); | ||
| return num >= 1 && num <= 120; | ||
| }, | ||
| { | ||
| message: "Age must be between 1 and 120", | ||
| }, | ||
| ), | ||
| }); | ||
|
|
||
| const extendedSchema = z.object({ | ||
| firstname: z.string().min(1, "Firstname is required"), | ||
| lastname: z.string().min(1, "Lastname is required"), | ||
| middlename: z.string().optional(), | ||
| age: z | ||
| .string() | ||
| .min(1, "Age is required") | ||
| .regex(/^\d+$/, { message: "Age must be a non-negative integer" }) | ||
| .refine( | ||
| (val) => { | ||
| const num = Number(val); | ||
| return num >= 1 && num <= 120; | ||
| }, | ||
| { | ||
| message: "Age must be between 1 and 120", | ||
| }, | ||
| ), | ||
| frequency: z.enum(["daily", "weekly", "monthly"], { | ||
| error: () => ({ message: "Invalid frequency" }), | ||
| }), | ||
| amenities: z | ||
| .array( | ||
| z.enum( | ||
| ["Audio", "Video", "White Board", "HDMI", "Projector", "Speaker Phone"], | ||
| { | ||
| error: () => ({ message: "Invalid amenities" }), | ||
| }, | ||
| ), | ||
| ) | ||
| .optional(), | ||
| recurrenceDate: z.enum(["Monday", "Tuesday", "Wednesday"], { | ||
| error: () => ({ message: "Invalid frequency" }), | ||
| }), | ||
| }); |
Copilot
AI
Dec 9, 2025
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.
The schema and extendedSchema contain duplicated validation logic for firstname, lastname, middlename, and age fields. Consider extracting the common fields into a base schema and extending it using z.object({...}).extend({...}) or composition to reduce code duplication.
client/src/app/test/form/page.tsx
Outdated
| ) | ||
| .optional(), | ||
| recurrenceDate: z.enum(["Monday", "Tuesday", "Wednesday"], { | ||
| error: () => ({ message: "Invalid frequency" }), |
Copilot
AI
Dec 9, 2025
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.
The error message for recurrenceDate says "Invalid frequency" which is inconsistent and misleading. It should say "Invalid recurrence date" or similar to match the field name.
| error: () => ({ message: "Invalid frequency" }), | |
| error: () => ({ message: "Invalid recurrence date" }), |
client/src/components/ui/form.tsx
Outdated
| @@ -0,0 +1,150 @@ | |||
| // Usage of Form: | |||
| // import { Form, ControlledField } from "@/components/ui/form"; // Note: both are necessary and not default export! | |||
| // <Form schema={zodSchema} // Note: according to the current implimentation, numbers are also string. For an example please refer to /app/test/form/page.tsx | |||
Copilot
AI
Dec 9, 2025
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.
The comment references "zodSchema" but the parameter is named "schema" in the actual component. This inconsistency could confuse developers using this documentation.
| // <Form schema={zodSchema} // Note: according to the current implimentation, numbers are also string. For an example please refer to /app/test/form/page.tsx | |
| // <Form schema={schema} // Note: according to the current implimentation, numbers are also string. For an example please refer to /app/test/form/page.tsx |
|
Copilot suggestion fixed. |
|
The component is rewritten with shadcn style. Please review again. Thanks. Again, no idea why the type check fails. The error message is like below: Error: src/app/test/form/page.tsx(16,8): error TS1192: Module '"/home/runner/work/bloom-booking-system/bloom-booking-system/client/src/components/ui/input"' has no default export. |
…Add detailed documentation about the usage of the components. Frontend styling and test to be done.
…e customization. Update tests and documentation.
…ype of value) => void and (type of event) => void.
…ype of value) => void and (type of event) => void.
…id to handle the issue that errors are empty at the beginning. Components remain intact but update the documentation.
…ove redundant empty fragment wrapper.
…ema definition and use schema.extend() to reduce redundancy in test file.
…r message display and api error handling.
eabaa1c to
9ad3479
Compare


Change Summary
Add form component with react-hook-form and yup.
Change Form
Fill this up (NA if not available). If a certain criteria is not met, can you please give a reason.
Note: please check out http://localhost:3000/test/form for simple tests. However I do think the test is incomplete for it does not test complex input types and design.
Other Information
Demo:
form_demo.mp4
Related issue