Layout

Order, sections and rows, placed components, and the markup formdef renders.

fields says what a level contains, layout says where it goes. Without a layout, a level renders its fields in the order fields declares them. With one, the layout decides the order and what appears at all.

const  = .({
  : .(),
  : .(),
  : .(),
  : .({ : .(), : .() }),
})

export const  = createForm({
  ,
  : { : '', : '', : '', : { : '', : '' } },
  fields: {
    firstName: { : 'text', : { : 'First name' } },
    lastName: { : 'text', : { : 'Last name' } },
    email: { : 'text', : { : 'Email' } },
    address: {
      : {
        zip: { : 'text', : { : 'ZIP code' } },
        city: { : 'text', : { : 'City' } },
      },
      : [{ : ['zip', 'city'] }],
    },
  },
  : [
    { : 'Customer', layout: [{ : ['firstName', 'lastName'] }, 'email'] },
    { : 'Address', : ['address'] },
    { : 'SubmitButton', : { : 'Save' } },
  ],
})

Names in a layout are keys of that level, never paths: the root layout places address, and the layout inside address places zip and city. A field a layout leaves out is not rendered, keeps its value and is still validated, exactly like a key fields leaves out.

The five layout entries

EntryRendersWithout a registered slot
'name'the field entry of this level—
{ section: 'Title', layout: [...] }a titled group<fieldset> with a <legend>
{ row: [...] }entries side by side<div data-formdef-row>
{ component: 'Name', props }a registered form component—
{ render: (form) => ... }an inline entry—

An entry is exactly one of these. A section with a component does not compile.

Sections and rows

Sections and rows hold layout entries, not just names, so they nest: a row inside a section, a section inside a section. Both take props, which go to the registered slot component, or, without one, a className for the bare element:

export const  = createForm({
  : .({ : .(), : .() }),
  : { : '', : '' },
  fields: {
    firstName: { : 'text', : { : 'First name' } },
    lastName: { : 'text', : { : 'Last name' } },
  },
  : [{ : ['firstName', 'lastName'], : { : 'grid grid-cols-2 gap-4' } }],
})

Form components and inline entries

Two entries place something that is not a field. Which one fits depends on whether it reads values.

A form component is registered once under formComponents and placed by name, its props checked against the component. It reads the form through useFormContext(), which does not know the form's data type. That suits pieces that read no values and are reused across forms, such as a submit button.

An inline entry is a render function in the layout. It receives the form as a FormHandle, typed with the form's data, so it suits anything that reads or writes a value:

export const  = createForm({
  : .({ : .(), : .() }),
  : { : '', : '' },
  fields: {
    firstName: { : 'text', : { : 'First name' } },
    lastName: { : 'text', : { : 'Last name' } },
  },
  : [
    'firstName',
    'lastName',
    {
      : () => (
        <.Subscribe selector={(state) => `${state..} ${state..}`}>
          {() => <p>Saved as {}</p>}
        </.Subscribe>
      ),
    },
  ],
})

The handle reads and writes values and field meta by path, subscribes to the form's state, submits, resets and changes arrays. The members whose types depend on the validators, such as validate, are not part of it; a form that needs them takes its own shell.

Inline entries also hold content that is not bound to the form at all, a heading or a divider: { render: () => <hr /> }.

The markup formdef renders

formdef renders markup of its own in four places. Each is a structure slot you can fill with a component when you create the registry:

SlotWhereWithout itReceives
bodyinside every <form> a registry buildsnothing; the fields sit in the <form>BodyProps: children
sectioneach { section } entry<fieldset><legend>SectionProps: label, children
roweach { row } entry<div data-formdef-row>RowProps: children
arrayFrameeach array entrythe items and their buttonsArrayFrameProps, see Arrays
const  = ({  }: BodyProps) => <div className="flex flex-col gap-6">{}</div>

const  = ({ ,  }: SectionProps) => (
  <fieldset className="flex flex-col gap-4">
    <legend className="font-medium">{}</legend>
    {}
  </fieldset>
)

const  = ({  }: RowProps) => <div className="grid gap-4 sm:grid-cols-2">{}</div>

export const { createForm, Fields } = ({
  ,
  ,
  : { :  },
  : { : , : , :  },
})

A slot component may take props of its own beyond these; a section or row entry then passes them through props, checked against the component. body takes none, since no entry describes it. When you build the <form> yourself with Fields, body is not applied: the shell is yours, and so is the wrapper.