Skip to content

Nested forms

insertSelectFormTree targets a branch of the form, and insertSubFormField declares a sub-form inside it — for state that is not flat.

Use them when the state has nested objects or arrays of objects. Not when the form is one level deep — attach insertFormAttributes directly.

insertSelectFormTree

Selects and composes nested sub-forms.

ts
interface ProductForm {
  name: string;
  variants: Array<{
    color: string;
    stock: number;
  }>;
}

const { productFormState } = state(
  'productFormState',
  { name: '', variants: [] } as ProductForm,
  insertForm(
    insertSelectFormTree(
      'variant',
      insertNoopTypingAnchor,
      insertFormAttributes(() => ({
        validators: [cRequired(), cMin({ min: 0 })],
      })),
    ),
  ),
);

// Access sub-forms
const form = productFormState.form();
const variant0 = form.selectVariant(0);
const allVariants = form.items();

Selection is lazy: calling selectVariant(...), items(), or an object selector such as selectEmail() materializes the selected branch and registers its insertions. Pass that selected field to DOM bindings; accessing the raw field tree alone does not run the branch insertions.

When an object branch has a group validator but no matching DOM control, materialize it in the component logic and return the selected group from the factory instead:

ts
const credentials = registration.form.selectCredentials();
return { registration, credentials };

Its typed validation cases then belong to the component contract without requiring CraftFieldDirective(credentials). Bind the leaf controls and handle the group path on an enclosing fieldExceptionBlock. See Form exception handling.

insertSubFormField

Exposes a derived sub-form from a parent value through a lens. This is useful when the form field is not stored as a nested object in the state, but can still be read and written from the parent value.

ts
import { state } from '@craft-ng/core';
import {
  insertForm,
  insertFormAttributes,
  insertSubFormField,
  splitLens,
  cRequired,
} from '@craft-ng/core';

const { appointmentFormState } = state(
  'appointmentFormState',
  '2026-05-10 12:00',
  insertForm(
    insertSubFormField(
      'date',
      splitLens(' ', 0),
      insertFormAttributes(() => ({
        validators: [cRequired()],
      })),
    ),
    insertSubFormField('time', splitLens(' ', 1)),
  ),
);

const form = appointmentFormState.form();
const dateField = form.selectDate();
const timeField = form.selectTime();

console.log(dateField.value()); // '2026-05-10'
console.log(timeField.value()); // '12:00'

dateField.set('2026-05-11');
timeField.set('09:30');

console.log(appointmentFormState()); // '2026-05-11 09:30'

See Also