Automatically generated forms beta

I’m excited to share a beta version of a component I’ve been working on in my free time (with input from the Redwood team) that’s designed to make creating forms a bit easier. The AutoForm component dynamically generates forms based on a given zod schema, saving you the time and effort of coding/styling/validating them by hand. You define the schema for your form; AutoForm takes it from there.

I’m looking for some feedback from folks who have real apps and real forms they’re maintaining/creating, and what better demographic than Fractal startups. A demo can be found on codesandbox with some instructions on how to play with it. Here’s a quick summary of notable points for folks.

  • :link: The demo can be found on codesandbox: https://githubbox.com/colbywhite/rw-form-generator. Fork the codesandbox and make some changes to get a feel for how using this component would look like.
  • :page_facing_up: Modify the schema in web/src/schemas/Demo.ts to see changes to the schema resulting in changes to <form>.
  • :page_facing_up: Modify the usage in web/src/components/AutoFormDemo to play with styling.
  • :page_facing_up: Take a look at web/src/components/AutoForm/README.md to see some examples on what’s currently supported. What kind of gaps would you need filled?
  • :smiling_imp: Try to break it! Try to recreate the kind of forms you use in your apps. The component doesn’t support every kind of schema today, but I want to know what kind of forms ppl would want to create with something like this. That’ll dictate what I focus on next.
  • :speaking_head: After playing with the demo, please share your feedback through the survey here: https://forms.gle/Vrb9UWqb6JR8Jx9T8
    • you can also always deliver feedback via this slack/discord/forum

I do want to call out that this is the first step in a bigger journey. Once the <form> itself is being created by a zod schema, then we can hook this up to Redwood generators and generate the zod schema on either a Prisma model or a GQL mutation (or whatever else is wanted). Imagine a rw g form MyEntity command that spits you out a form component. Ideally, that covers 80% of your form needs, but the zod schema is still around for you to tweak to get you through that last mile of edge cases. (That’s why a zod-as-middleman strategy was chosen.)

Or at least, that’s the grand plan. Your input is important in shaping that strategy. Let me know how valuable this stuff would be for you, and we (me + the RW team) will iterate on it over time.

Thanks for the feedback! :pray:

7 Likes

This looks super cool!!

With a simpler form, like the one in the example, rather than dealing with all the overhead, I would prefer to just do this, where everything related to field rendering (styling, label, errors…) is handled in those Field components (like in the demo on the components library):

interface IFormCreateEntity {
  email: string
  name?: string
}

const onSubmit = (data: IFormCreateEntity) => {
  // do something with the data
}

return (
  <Form<IFormCreateEntity> onSubmit={onSubmit}>
    <TextField name="email" label="Email" />
    <TextField name="name" label="Name" optional /> // fields should be required by default
    <Submit>Create</Submit>
  </Form>
)

Where I would then find AutoForm useful is where the effort of manual declaration is greater than that of using AutoForm - with a schema like this, for example (the Spoonjoy recipe input):

const StringSchema = z.string();
const IntSchema = z.number().int();
const FloatSchema = z.number();
const BooleanSchema = z.boolean();

const CreateIngredientInput = z.object({
  quantity: FloatSchema,
  unitName: StringSchema,
  ingredientName: StringSchema,
}).required();

const CreateStepOutputUseInput = z.object({
  outputOfStepNum: IntSchema,
  inputOfStepNum: IntSchema.optional(),
}).required();

const CreateRecipeStepInput = z.object({
  description: StringSchema,
  stepNum: IntSchema,
  stepTitle: StringSchema.optional(),
  ingredients: z.array(CreateIngredientInput).optional(),
  stepOutputUses: z.array(CreateStepOutputUseInput).optional(),
}).required();

const CreateRecipeInput = z.object({
  description: StringSchema.optional(),
  title: StringSchema,
  servings: IntSchema.optional(),
  steps: z.array(CreateRecipeStepInput),
}).required();

const CreateRecipeInputSchema = z.object({
  input: CreateRecipeInput,
});

I did try to test that out, but it looks like something isn’t yet supported by AutoField:

That is a form that took a lot of time to manually define, and I’d love to see what AutoForm would be able to do for a situation like that.

Keep up the great work! :slight_smile:

Nested objects aren’t supported quite yet but are on the list of potential next additions. Ideally, it would result it a <fieldset> & <legend> layout. sounds like nested objects would be a big win. good to know. :+1:

1 Like

Yeah, and arrays too! I have lots of useFieldArray in my forms.

What’s the runtime complexity of AutoForm?

:loudspeaker: Update: custom field components supported :loudspeaker:

Quick update: I’ve expanded the override property to add support for custom fields to this project. Check out the codesandbox to see it in action. The demo uses the datalist strategy as an example, but it’s a custom field so any react-hook-form based field can be passed in to handle unique scenarios. This feels like a good escape hatch for when the zod validation just doesn’t match cleanly to a native form element.

    <AutoForm
      aria-label="Custom component override usage"
      overrides={{
        flavor: (props) => (
          <DataListInputField {...props} options={['Chocolate', 'Coconut', 'Caramel', 'Cherry']} />
        )
      }}
      schema={schema}
      onSubmit={onSubmit}>
    </AutoForm>

Feedback encouraged as always!

2 Likes

Arrays in the context of multiple options are covered by the mapping of z.enum to <input type="radio"> and z.enum.array to <input type="checkbox">

but i assume you’re referring to a dynamic form use case. which makes a lot of sense. thinking through that, would it be a schema like this?

const schema = z.object({
  cart: z.array(
    z.object({
      name: z.string(),
      price: z.number()
    })
  )
})

i’m pretty sure i can detect that it’s an array of objects in the zod schema. but i think the question would be what would be a simple sensible UI be for that? some use cases would need an append button in the fieldset, others a prepend, some both. maybe all need a delete. :thinking: what’s your html structure for your use cases?

1 Like

Yeah, it can get pretty hairy, especially with nested arrays, and different fieldarrays have different purposes. In Spoonjoy’s case, a recipe has steps, and a step has ingredients. So the form looks like this:

(Video might still be processing)

I’m also curious on runtime complexity - when does form generation happen? It’s not something that needs to happen every single time, so perhaps somehow we should keep it to compile time?

perhaps somehow we should keep it to compile time?

right now it’s a runtime op in the AutoForm component. doing it at compile time is interesting idea.

i haven’t yet started playing with RSC tho. I assume doing this in a RSC-env would be fine

1 Like

Yeah, once AutoForm has support for more complex forms we should definitely see how it affects loading times compared to pre-defined forms

1 Like

:loudspeaker: Update: better support for required/optional fields :loudspeaker:

Due to the way HTML forms work (i.e. doing nothing in an <input> usually results in a "" value), the state of “the user has not entered anything” may get represented in unintuitive ways when stitching together zod, react-hook-form (which is what @redwoodjs/forms is built on), and @hookform/resolvers.

I’ve updated the lib to document the ways you can work around this oddity with the above tools (see web/src/components/AutoForm/README.md while adding support for those workarounds.

1 Like

:loudspeaker: Update: added support for object fields :loudspeaker:

I’ve added support for objects in the schema. An object will now result in a <fieldset> with a dot-separated name.

As an example, the following schema

const schema = z.object({
  name: z.object({
    first: z.string().min(1, 'First name is required'),
    last: z.string().min(1, 'Last name is required'),
  })
})

would result in the following DOM

<form aria-label="demo form">
  <fieldset>
    <label>
      name.first 
      <input id="name.first" type="text" name="name.first" />
    </label>
    <label>
      name.last 
      <input id="name.last" type="text" name="name.last" />
    </label>
  </fieldset>
</form>

Displaying the appropriate label for your use case would be done via the Label property.

Feedback encouraged as always!

1 Like

:loudspeaker: Release candidate now available; docs site up :loudspeaker:

I’ve done the CI rigmarole and have released a release candidate: @colbyw/autoform@1.0.0-rc.2. This means you can start using this in your projects now. The thing I’m looking for before elevating this beyond a RC is some confirmation from someone not named Colby that it works on their machine.

I’ve also took the loose MD docs and made a better doc site for the lib: https://redwood-autoform.netlify.app. This should make it easier to grok the usage cases. (Note: as I type this I now realize that I need to replace the default docusaurus logo that’s on the docs site. i’ll AI something together when i do the official release.)

Feedback encouraged as always!