AutoForm

A React component that automatically creates a @shadcn/ui form based on a z...

README

<AutoForm /> for @shadcn/ui


AutoForm is a React component that automatically creates a @shadcn/ui form based on a zod schema.

A live demo can be found at .

Installation


The component depends on the following components from shadcn/ui:

- accordion
- button
- calendar
- card
- checkbox
- form
- input
- label
- popover
- radio-group
- select
- separator
- switch
- textarea
- toggle

You can install them all at once with:

  1. ```bash
  2. npx shadcn-ui@latest add accordion button calendar card checkbox form input label popover radio-group select separator switch textarea toggle
  3. ```

To install the component itself, copy auto-form.tsx and date-picker.tsx from src/components/ui to your project's ui folder.

Field types


Currently, these field types are supported out of the box:

- boolean (checkbox, switch)
- date (date picker)
- enum (select, radio group)
- number (input)
- string (input, textfield)

You can add support for other field types by adding them to the INPUT_COMPONENTS object in auto-form.tsx.

Usage


Basic usage:

  1. ```tsx
  2. "use client";
  3. import AutoForm, { AutoFormSubmit } from "./components/ui/auto-form";
  4. import * as z from "zod";

  5. // Define your form schema using zod
  6. const formSchema = z.object({
  7.   username: z
  8.     .string({
  9.       required_error: "Username is required.",
  10.     })
  11.     // You can use zod's built-in validation as normal
  12.     .min(2, {
  13.       message: "Username must be at least 2 characters.",
  14.     }),

  15.   password: z
  16.     .string({
  17.       required_error: "Password is required.",
  18.     })
  19.     // Use the "describe" method to set the label
  20.     // If no label is set, the field name will be used
  21.     // and un-camel-cased
  22.     .describe("Your secure password")
  23.     .min(8, {
  24.       message: "Password must be at least 8 characters.",
  25.     }),

  26.   favouriteNumber: z.coerce // When using numbers and dates, you must use coerce
  27.     .number({
  28.       invalid_type_error: "Favourite number must be a number.",
  29.     })
  30.     .min(1, {
  31.       message: "Favourite number must be at least 1.",
  32.     })
  33.     .max(10, {
  34.       message: "Favourite number must be at most 10.",
  35.     })
  36.     .default(5) // You can set a default value
  37.     .optional(),

  38.   acceptTerms: z
  39.     .boolean()
  40.     .describe("Accept terms and conditions.")
  41.     .refine((value) => value, {
  42.       message: "You must accept the terms and conditions.",
  43.       path: ["acceptTerms"],
  44.     }),

  45.   // Date will show a date picker
  46.   birthday: z.coerce.date().optional(),

  47.   sendMeMails: z.boolean().optional(),

  48.   // Enum will show a select
  49.   color: z.enum(["red", "green", "blue"]),

  50.   // Create sub-objects to create accordion sections
  51.   address: z.object({
  52.     street: z.string(),
  53.     city: z.string(),
  54.     zip: z.string(),
  55.   }),
  56. });

  57. function App() {
  58.   return (
  59.     <AutoForm
  60.       // Pass the schema to the form
  61.       formSchema={formSchema}
  62.       // You can add additional config for each field
  63.       // to customize the UI
  64.       fieldConfig={{
  65.         password: {
  66.           // Use "inputProps" to pass props to the input component
  67.           // You can use any props that the component accepts
  68.           inputProps: {
  69.             type: "password",
  70.             placeholder: "••••••••",
  71.           },
  72.         },
  73.         favouriteNumber: {
  74.           // Set a "description" that will be shown below the field
  75.           description: "Your favourite number between 1 and 10.",
  76.         },
  77.         acceptTerms: {
  78.           inputProps: {
  79.             required: true,
  80.           },
  81.           // You can use JSX in the description
  82.           description: (
  83.             <>
  84.               I agree to the{" "}
  85.               <a
  86.                 href="#"
  87.                 className="text-primary underline"
  88.                 onClick={(e) => {
  89.                   e.preventDefault();
  90.                   alert("Terms and conditions clicked.");
  91.                 }}
  92.               >
  93.                 terms and conditions
  94.               </a>
  95.               .
  96.             </>
  97.           ),
  98.         },

  99.         birthday: {
  100.           description: "We need your birthday to send you a gift.",
  101.         },

  102.         sendMeMails: {
  103.           // Booleans use a checkbox by default, you can use a switch instead
  104.           fieldType: "switch",
  105.         },
  106.       }}
  107.     >
  108.       {/*
  109.       Pass in a AutoFormSubmit or a button with type="submit".
  110.       Alternatively, you can not pass a submit button
  111.       to create auto-saving forms etc.
  112.       */}
  113.       <AutoFormSubmit>Send now</AutoFormSubmit>

  114.       {/*
  115.       All children passed to the form will be rendered below the form.
  116.       */}
  117.       <p className="text-gray-500 text-sm">
  118.         By submitting this form, you agree to our{" "}
  119.         <a href="#" className="text-primary underline">
  120.           terms and conditions
  121.         </a>
  122.         .
  123.       </p>
  124.     </AutoForm>
  125.   );
  126. }
  127. ```

Next.js and RSC


AutoForm can only be used inside a client-side React component due to serialization of the zod schema and values to your event listeners. If you want to use it in a Next.js app, simply mark your component with "use client":

  1. ```tsx
  2. // MyPage.tsx
  3. export default function MyPage() {
  4.   return (
  5.     <div>
  6.       <MyForm />
  7.     </div>
  8.   );
  9. }

  10. // MyForm.tsx
  11. "use client";
  12. import AutoForm from "./components/ui/auto-form";
  13. export default function MyForm() {
  14.   return <AutoForm onSubmit={...} ... />;
  15. }
  16. ```

Zod configuration


Validations


Your form schema can use any of zod's validation methods including refine.

Autoform is able to automatically transform some of zod's validation elements into HTML attributes. For example, if you use zod.string().min(8), the input will automatically have a minlength="8" attribute.

Validation methods that are not supported by HTML will automatically be checked when the form is submitted.

Descriptions


You can use the describe method to set a label and description for each field. If no label is set, the field name will be used and un-camel-cased.

  1. ```tsx
  2. const formSchema = z.object({
  3.   username: z.string().describe("Your username"),
  4.   someValue: z.string(), // Will be "Some Value"
  5. });
  6. ```

Coercion


When using numbers and dates, you should use coerce. This is because input elements may return a string that should automatically be converted.

  1. ```tsx
  2. const formSchema = z.object({
  3.   favouriteNumber: z.coerce.number(),
  4.   birthday: z.coerce.date(),
  5. });
  6. ```

Optional fields


By default, all fields are required. You can make a field optional by using the optional method.

  1. ```tsx
  2. const formSchema = z.object({
  3.   username: z.string().optional(),
  4. });
  5. ```

Default values


You can set a default value for a field using the default method.

  1. ```tsx
  2. const formSchema = z.object({
  3.   favouriteNumber: z.number().default(5),
  4. });
  5. ```

Sub-objects


You can nest objects to create accordion sections.

  1. ```tsx
  2. const formSchema = z.object({
  3.   address: z.object({
  4.     street: z.string(),
  5.     city: z.string(),
  6.     zip: z.string(),

  7.     // You can nest objects as deep as you want
  8.     nested: z.object({
  9.       foo: z.string(),
  10.       bar: z.string(),

  11.       nested: z.object({
  12.         foo: z.string(),
  13.         bar: z.string(),
  14.       }),
  15.     }),
  16.   }),
  17. });
  18. ```

Like with normal objects, you can use the describe method to set a label and description for the section:

  1. ```tsx
  2. const formSchema = z.object({
  3.   address: z
  4.     .object({
  5.       street: z.string(),
  6.       city: z.string(),
  7.       zip: z.string(),
  8.     })
  9.     .describe("Your address"),
  10. });
  11. ```

Select/Enums


AutoForm supports enum and nativeEnum to create select fields.

  1. ```tsx
  2. const formSchema = z.object({
  3.   color: z.enum(["red", "green", "blue"]),
  4. });

  5. enum BreadTypes {
  6.   // For native enums, you can alternatively define a backed enum to set a custom label
  7.   White = "White bread",
  8.   Brown = "Brown bread",
  9.   Wholegrain = "Wholegrain bread",
  10.   Other,
  11. }
  12. const formSchema = z.object({
  13.   bread: z.nativeEnum(BreadTypes),
  14. });
  15. ```

Arrays


AutoForm supports arrays _of objects_. Because inferring things like field labels from arrays of strings/numbers/etc. is difficult, only objects are supported.

  1. ```tsx
  2. const formSchema = z.object({
  3.   guestListName: z.string(),
  4.   invitedGuests: z
  5.     .array(
  6.       // Define the fields for each item
  7.       z.object({
  8.         name: z.string(),
  9.         age: z.coerce.number(),
  10.       })
  11.     )
  12.     // Optionally set a custom label - otherwise this will be inferred from the field name
  13.     .describe("Guests invited to the party"),
  14. });
  15. ```

Arrays are not supported as the root element of the form schema.

Field configuration


As zod doesn't allow adding other properties to the schema, you can use the fieldConfig prop to add additional configuration for the UI of each field.

  1. ```tsx
  2. <AutoForm
  3.   fieldConfig={{
  4.     // Add config for each field here - don't add the field name to keep all defaults
  5.     username: {
  6.       // Configuration here
  7.     },
  8.   }}
  9. />
  10. ```

Input props


You can use the inputProps property to pass props to the input component. You can use any props that the HTML component accepts.

  1. ```tsx
  2. <AutoForm
  3.   fieldConfig={{
  4.     username: {
  5.       inputProps: {
  6.         type: "text",
  7.         placeholder: "Username",
  8.       },
  9.     },
  10.   }}
  11. />

  12. // This will be rendered as:
  13. <input type="text" placeholder="Username" /* ... */ />
  14. ```

Field type


By default, AutoForm will use the Zod type to determine which input component to use. You can override this by using the fieldType property.

  1. ```tsx
  2. <AutoForm
  3.   fieldConfig={{
  4.     sendMeMails: {
  5.       // Booleans use a checkbox by default, use a switch instead
  6.       fieldType: "switch",
  7.     },
  8.   }}
  9. />
  10. ```

The complete list of supported field types is typed. Current supported types are:

- "checkbox" (default for booleans)
- "switch"
- "date" (default for dates)
- "select" (default for enums)
- "radio"
- "textarea"
- "fallback" (default for everything else, simple input field)

Alternatively, you can pass a React component to the fieldType property to use a custom component.

  1. ```tsx
  2. <AutoForm
  3.   fieldConfig={{
  4.     sendMeMails: {
  5.       fieldType: ({
  6.         label,
  7.         isRequired,
  8.         field,
  9.         fieldConfigItem,
  10.         fieldProps,
  11.       }: AutoFormInputComponentProps) => (
  12.         <FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md border p-4">
  13.           <FormControl>
  14.             <Switch
  15.               checked={field.value}
  16.               onCheckedChange={field.onChange}
  17.               {...fieldProps}
  18.             />
  19.           </FormControl>
  20.           <div className="space-y-1 leading-none">
  21.             <FormLabel>
  22.               {label}
  23.               {isRequired && <span className="text-destructive"> *}
  24.             </FormLabel>
  25.             {fieldConfigItem.description && (
  26.               <FormDescription>{fieldConfigItem.description}</FormDescription>
  27.             )}
  28.           </div>
  29.         </FormItem>
  30.       ),
  31.     },
  32.   }}
  33. />
  34. ```

Description


You can use the description property to add a description below the field.

  1. ```tsx
  2. <AutoForm
  3.   fieldConfig={{
  4.     username: {
  5.       description:
  6.         "Enter a unique username. This will be shown to other users.",
  7.     },
  8.   }}
  9. />
  10. ```

You can use JSX in the description.

Custom parent component


You can use the renderParent property to customize the parent element of the input to add adornments etc.
By default, this is a React fragment.

  1. ```tsx
  2. <AutoForm
  3.   fieldConfig={{
  4.     username: {
  5.       renderParent: ({ children }) => (
  6.         <div className="flex items-end gap-3">
  7.           <div className="flex-1">
  8.             {children} // This is the input with label etc.
  9.           </div>
  10.           <div>
  11.             <Button type="button">Check</Button>
  12.           </div>
  13.         </div>
  14.       ),
  15.     },
  16.   }}
  17. />
  18. ```

Accessing the form data


There are two ways to access the form data:

onSubmit


The preferred way is to use the onSubmit prop. This will be called when the form is submitted and the data is valid.

  1. ```tsx
  2. <AutoForm
  3.   onSubmit={(data) => {
  4.     // Do something with the data
  5.     // Data is validated and coerced with zod automatically
  6.   }}
  7. />
  8. ```

Controlled form


You can also use the values and onValuesChange props to control the form data yourself.

  1. ```tsx
  2. const [values, setValues] = useState<Partial<z.infer<typeof formSchema>>>({});

  3. <AutoForm values={values} onValuesChange={setValues} />;
  4. ```

Please note that the data is not validated or coerced when using this method as they update immediately.

Submitting the form


You can use the AutoFormSubmit component to create a submit button.

  1. ```tsx
  2. <AutoForm>
  3.   <AutoFormSubmit>Send now</AutoFormSubmit>
  4. </AutoForm>
  5. // or
  6. <AutoForm>
  7.   <button type="submit">Send now</button>
  8. </AutoForm>
  9. ```

Adding other elements


All children passed to the AutoForm component will be rendered below the form.

  1. ```tsx
  2. <AutoForm>
  3.   <AutoFormSubmit>Send now</AutoFormSubmit>
  4.   <p className="text-gray-500 text-sm">
  5.     By submitting this form, you agree to our{" "}
  6.     <a href="#" className="text-primary underline">
  7.       terms and conditions
  8.     </a>
  9.     .
  10.   </p>
  11. </AutoForm>
  12. ```

Contributing


Contributions are welcome! Please open an issue or submit a pull request.

1. Fork the repository
1. Clone your fork and install dependencies with npm install
1. Run npm run dev to start the development server and make your changes
1. Run npm run fix to run the formatter and linter
1. Commit your changes and open a pull request

License


MIT