Skip to content

Nested & Repeatable Fields ​

Two field types let you compose a DTO out of smaller pieces instead of a single flat list: "group" for a fixed set of related fields, and "array" for a repeatable list of them.


"group" — a labeled cluster of fields ​

A group is purely a visual and logical grouping — it does not change how values are stored. Each nested field still gets its own top-level id in the form's flat values object, exactly as if it had been declared directly on the section.

ts
import type { FormDTO } from 'react-form-dto';

const profileForm: FormDTO = {
  sections: [
    {
      id: 'main',
      fields: [
        {
          id: 'address',
          type: 'group',
          label: 'Address',
          fields: [
            {
              id: 'street',
              type: 'text',
              label: 'Street',
              validations: { required: 'Street is required' },
            },
            { id: 'city', type: 'text', label: 'City' },
          ],
        },
      ],
    },
  ],
};

// After submit, `data` looks like: { street: '...', city: '...' }
// — NOT { address: { street: '...', city: '...' } }.

Because nested fields keep flat top-level ids:

  • Their validations run exactly like any other field's (including as part of form.handleSubmit/form.trigger()).
  • visibleWhen works on nested fields too, and can reference fields both inside and outside the group.
  • ids must still be unique across the entire DTO, not just within the group.

"array" — a repeatable list of items ​

An array field renders zero or more copies of an item template (fields), with built-in Add/Remove controls. Unlike "group", the whole list is stored as a single value:

ts
const contactsForm: FormDTO = {
  sections: [
    {
      id: 'main',
      fields: [
        {
          id: 'contacts',
          type: 'array',
          label: 'Contacts',
          fields: [
            { id: 'name', type: 'text', label: 'Name' },
            { id: 'email', type: 'email', label: 'Email' },
          ],
        },
      ],
    },
  ],
};

// values.contacts is then a list of { name, email } objects

Each item's fields are scoped to that item (no id clashes between items or with the rest of the form — name/email here don't collide with a top-level name field elsewhere).

Known limitations (for now) ​

  • Per-item validation isn't wired into validateAll/trigger() yet — validate array contents with a top-level validate function on a field, or with a schema resolver, until per-item DTO validation lands.
  • visibleWhen conditions cannot reference a specific array item (e.g. "item 0's type").
  • Custom renderers for the item template's field types are respected (passed through via renderers), but there's currently no way to override the array/group container's own layout.

Custom rendering ​

Both types are ordinary field renderers under the hood — GroupField and ArrayField, exported from the package — so you can override either the same way you'd override any other field type:

tsx
<FormBuilder dto={myDTO} renderers={{ array: MyCustomArrayRenderer }} />