diff --git a/apps/docs/pages/docs/react/api.mdx b/apps/docs/pages/docs/react/api.mdx index dba7814..fa23239 100644 --- a/apps/docs/pages/docs/react/api.mdx +++ b/apps/docs/pages/docs/react/api.mdx @@ -35,3 +35,7 @@ Additional, custom form components can be passed to the `formComponents` prop. T ## uiComponents: Partial\ Override the default UI components with custom components. This allows you to customize the look and feel of the form. + +## formProps?: React.ComponentProps\<'form'> + +The `formProps` prop allows you to pass additional props directly to the underlying form element. This can be useful for adding custom attributes, event handlers, or other properties to the form. diff --git a/apps/docs/pages/docs/react/customization.md b/apps/docs/pages/docs/react/customization.md index 1f613d5..f464b2c 100644 --- a/apps/docs/pages/docs/react/customization.md +++ b/apps/docs/pages/docs/react/customization.md @@ -199,3 +199,24 @@ You can also override the default UI components with custom components. This all }} /> ``` + +## Form element customization + +In addition to overriding UI components, you can also customize the form element itself using the `formProps` prop: + +```tsx + { + console.log("onTouchStart", e); + }, + }} +/> +``` + +This allows you to add custom classes, data attributes, or other properties directly to the form element. diff --git a/apps/docs/pages/docs/react/getting-started.mdx b/apps/docs/pages/docs/react/getting-started.mdx index d35e170..58123a7 100644 --- a/apps/docs/pages/docs/react/getting-started.mdx +++ b/apps/docs/pages/docs/react/getting-started.mdx @@ -122,6 +122,25 @@ const [values, setValues] = useState({}); This allows you to access form methods and state from the parent component. You can use the `onFormInit` prop independent of controlled forms to access the form instance. +## Passing props to the form element + +You can pass additional props to the underlying form element using the `formProps` prop: + +```tsx + { + if (e.key === "Enter") e.preventDefault(); + }, + }} +/> +``` + +This allows you to customize the form element's attributes and behavior as needed. + ## Submitting the form You can use the `withSubmit` prop to automatically add a submit button to the form. diff --git a/apps/web/cypress/component/autoform/mantine-zod/form-props.cy.tsx b/apps/web/cypress/component/autoform/mantine-zod/form-props.cy.tsx new file mode 100644 index 0000000..221232e --- /dev/null +++ b/apps/web/cypress/component/autoform/mantine-zod/form-props.cy.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { AutoForm } from "@autoform/mantine"; +import { ZodProvider } from "@autoform/zod"; +import { z } from "zod"; +import { TestWrapper } from "./utils"; + +describe("AutoForm Form Props Tests (Mantine)", () => { + const schema = z.object({ + name: z.string(), + }); + + const schemaProvider = new ZodProvider(schema); + + it("applies custom form props", () => { + cy.mount( + + + + ); + + cy.get("form") + .should("have.class", "custom-form-class") + .and("have.attr", "data-testid", "custom-form"); + + cy.get('input[name="name"]').type("{enter}"); + cy.get("@onKeyDown").should("have.been.called"); + }); + + it("prevents form submission on enter key", () => { + cy.mount( + + { + if (e.key === "Enter") e.preventDefault(); + }, + }} + /> + + ); + + cy.get('input[name="name"]').type("John Doe{enter}"); + cy.get("@onSubmit").should("not.have.been.called"); + }); +}); diff --git a/apps/web/cypress/component/autoform/mui-zod/form-props.cy.tsx b/apps/web/cypress/component/autoform/mui-zod/form-props.cy.tsx new file mode 100644 index 0000000..94d279d --- /dev/null +++ b/apps/web/cypress/component/autoform/mui-zod/form-props.cy.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import { AutoForm } from "@autoform/mui"; +import { ZodProvider } from "@autoform/zod"; +import { z } from "zod"; + +describe("AutoForm Form Props Tests (MUI)", () => { + const schema = z.object({ + name: z.string(), + }); + + const schemaProvider = new ZodProvider(schema); + + it("applies custom form props", () => { + cy.mount( + + ); + + cy.get("form") + .should("have.class", "custom-form-class") + .and("have.attr", "data-testid", "custom-form"); + + cy.get('input[name="name"]').type("{enter}"); + cy.get("@onKeyDown").should("have.been.called"); + }); + + it("prevents form submission on enter key", () => { + cy.mount( + { + if (e.key === "Enter") e.preventDefault(); + }, + }} + /> + ); + + cy.get('input[name="name"]').type("John Doe{enter}"); + cy.get("@onSubmit").should("not.have.been.called"); + }); +}); diff --git a/apps/web/cypress/component/autoform/shadcn-zod/form-props.cy.tsx b/apps/web/cypress/component/autoform/shadcn-zod/form-props.cy.tsx new file mode 100644 index 0000000..9343af0 --- /dev/null +++ b/apps/web/cypress/component/autoform/shadcn-zod/form-props.cy.tsx @@ -0,0 +1,57 @@ +import React from "react"; +import { AutoForm } from "@autoform/shadcn/components/ui/autoform/AutoForm"; +import { ZodProvider } from "@autoform/zod"; +import { z } from "zod"; +import { TestWrapper } from "./utils"; + +describe("AutoForm Form Props Tests (shadcn)", () => { + const schema = z.object({ + name: z.string(), + }); + + const schemaProvider = new ZodProvider(schema); + + it("applies custom form props", () => { + cy.mount( + + + + ); + + cy.get("form") + .should("have.class", "custom-form-class") + .and("have.attr", "data-testid", "custom-form"); + + cy.get('input[name="name"]').type("{enter}"); + cy.get("@onKeyDown").should("have.been.called"); + }); + + it("prevents form submission on enter key", () => { + cy.mount( + + { + if (e.key === "Enter") e.preventDefault(); + }, + }} + /> + + ); + + cy.get('input[name="name"]').type("John Doe{enter}"); + cy.get("@onSubmit").should("not.have.been.called"); + }); +}); diff --git a/package-lock.json b/package-lock.json index f9a5e67..4a2e482 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ } }, "apps/docs": { - "version": "0.1.0", + "version": "1.0.0", "license": "MIT", "dependencies": { "@autoform/core": "*", @@ -354,7 +354,7 @@ } }, "apps/web": { - "version": "0.2.0", + "version": "1.0.0", "dependencies": { "@autoform/mantine": "*", "@autoform/mui": "*", @@ -19783,7 +19783,7 @@ }, "packages/core": { "name": "@autoform/core", - "version": "1.2.0", + "version": "2.0.0", "devDependencies": { "@autoform/eslint-config": "*", "@autoform/typescript-config": "*", @@ -19799,7 +19799,7 @@ }, "packages/eslint-config": { "name": "@autoform/eslint-config", - "version": "0.0.0", + "version": "1.0.0", "dev": true, "devDependencies": { "@typescript-eslint/eslint-plugin": "^7.1.0", @@ -19813,7 +19813,7 @@ }, "packages/mantine": { "name": "@autoform/mantine", - "version": "1.2.0", + "version": "2.0.0", "dependencies": { "@autoform/core": "*", "@autoform/react": "*", @@ -19840,7 +19840,7 @@ }, "packages/mui": { "name": "@autoform/mui", - "version": "1.2.0", + "version": "2.0.0", "dependencies": { "@autoform/core": "*", "@autoform/react": "*", @@ -20054,7 +20054,7 @@ }, "packages/react": { "name": "@autoform/react", - "version": "1.3.0", + "version": "2.0.0", "dependencies": { "@autoform/core": "*", "@autoform/yup": "*", @@ -20078,7 +20078,7 @@ }, "packages/shadcn": { "name": "@autoform/shadcn", - "version": "1.1.0", + "version": "2.0.0", "dependencies": { "@autoform/core": "*", "@autoform/react": "*", @@ -20144,7 +20144,7 @@ }, "packages/typescript-config": { "name": "@autoform/typescript-config", - "version": "0.0.0", + "version": "1.0.0", "dev": true, "license": "MIT" }, @@ -20169,7 +20169,7 @@ }, "packages/yup": { "name": "@autoform/yup", - "version": "1.1.0", + "version": "2.0.0", "dependencies": { "@autoform/core": "*", "yup": "^1.4.0" @@ -20189,7 +20189,7 @@ }, "packages/zod": { "name": "@autoform/zod", - "version": "1.2.0", + "version": "2.0.0", "dependencies": { "@autoform/core": "*", "zod": "^3.23.8" diff --git a/packages/mantine/src/components/Form.tsx b/packages/mantine/src/components/Form.tsx index 1e43f00..3a9c421 100644 --- a/packages/mantine/src/components/Form.tsx +++ b/packages/mantine/src/components/Form.tsx @@ -1,21 +1,22 @@ import React from "react"; import { Box } from "@mantine/core"; -export const Form: React.FC<{ - onSubmit: (e: React.FormEvent) => void; - children: React.ReactNode; -}> = ({ onSubmit, children }) => { +export const Form = React.forwardRef< + HTMLFormElement, + React.ComponentProps<"form"> +>(({ children, ...props }, ref) => { return ( {children} ); -}; +}); diff --git a/packages/mui/src/components/Form.tsx b/packages/mui/src/components/Form.tsx index 7a61d7c..f6e6ce9 100644 --- a/packages/mui/src/components/Form.tsx +++ b/packages/mui/src/components/Form.tsx @@ -1,13 +1,13 @@ import React from "react"; import Box from "@mui/material/Box"; -export const Form: React.FC<{ - onSubmit: (e: React.FormEvent) => void; - children: React.ReactNode; -}> = ({ onSubmit, children }) => { +export const Form = React.forwardRef< + HTMLFormElement, + React.ComponentProps<"form"> +>(({ children, ...props }, ref) => { return ( - + {children} ); -}; +}); diff --git a/packages/react/src/AutoForm.tsx b/packages/react/src/AutoForm.tsx index 99ca1ac..606ba25 100644 --- a/packages/react/src/AutoForm.tsx +++ b/packages/react/src/AutoForm.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, { FormEventHandler, useEffect } from "react"; import { useForm, FormProvider, DefaultValues } from "react-hook-form"; import { parseSchema, @@ -19,6 +19,7 @@ export function AutoForm>({ formComponents, withSubmit = false, onFormInit = () => {}, + formProps = {}, }: AutoFormProps) { const parsedSchema = parseSchema(schema); const methods = useForm({ @@ -71,7 +72,10 @@ export function AutoForm>({ formComponents, }} > - + {parsedSchema.fields.map((field) => ( ))} diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index a7e9885..0224072 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -23,6 +23,7 @@ export interface AutoFormProps { formComponents: AutoFormFieldComponents; withSubmit?: boolean; onFormInit?: (form: UseFormReturn) => void; + formProps?: React.ComponentProps<"form"> & Record; } export type ExtendableAutoFormProps = Omit< @@ -34,10 +35,7 @@ export type ExtendableAutoFormProps = Omit< }; export interface AutoFormUIComponents { - Form: React.ComponentType<{ - onSubmit: (e: React.FormEvent) => void; - children: ReactNode; - }>; + Form: React.ComponentType>; FieldWrapper: React.ComponentType; ErrorMessage: React.ComponentType<{ error: string }>; SubmitButton: React.ComponentType<{ children: ReactNode }>; diff --git a/packages/shadcn/registry/autoform.json b/packages/shadcn/registry/autoform.json index 5cc1a6f..0f174f1 100644 --- a/packages/shadcn/registry/autoform.json +++ b/packages/shadcn/registry/autoform.json @@ -83,7 +83,7 @@ { "path": "autoform/components/Form.tsx", "target": "components/ui/autoform/components/Form.tsx", - "content": "import React from \"react\";\n\nexport const Form: React.FC<{\n onSubmit: (e: React.FormEvent) => void;\n children: React.ReactNode;\n}> = ({ onSubmit, children }) => {\n return (\n
\n {children}\n
\n );\n};\n", + "content": "import React from \"react\";\n\nexport const Form = React.forwardRef<\n HTMLFormElement,\n React.ComponentProps<\"form\">\n>(({ children, ...props }, ref) => {\n return (\n
\n {children}\n
\n );\n});\n", "type": "registry:ui" }, { diff --git a/packages/shadcn/src/components/ui/autoform/components/Form.tsx b/packages/shadcn/src/components/ui/autoform/components/Form.tsx index 6a01c8f..bbb7396 100644 --- a/packages/shadcn/src/components/ui/autoform/components/Form.tsx +++ b/packages/shadcn/src/components/ui/autoform/components/Form.tsx @@ -1,12 +1,12 @@ import React from "react"; -export const Form: React.FC<{ - onSubmit: (e: React.FormEvent) => void; - children: React.ReactNode; -}> = ({ onSubmit, children }) => { +export const Form = React.forwardRef< + HTMLFormElement, + React.ComponentProps<"form"> +>(({ children, ...props }, ref) => { return ( -
+ {children}
); -}; +});