Skip to content

Guides

Nested forms

An address block, an emergency contact, a set of company details — some fragments show up in a dozen forms. Rather than copying them, a nested-form brick references another form. It is resolved and inlined at render time, so everything downstream — rules, validation, recap — sees one ordinary spec.

The brick carries a specRef in its configs. What the ref means is entirely up to the service that resolves it: a URL, a database id, a slug.

A brick that pulls in the address form
{
"type": "panel",
"id": "nested-form",
"name": "Nested form",
"configs": {
"key": "address",
"label": "Delivery address",
"specRef": "address-v2"
}
}

At render time that node is replaced by the referenced form’s children, wrapped in a labelled group.

The default FetchSpecSourceService treats the ref as a URL and supports baseUrl, headers, credentials and per-URL caching. To resolve refs from your own store, provide anything matching the interface:

  1. Implement fetchSpec, returning a BrickSpec.

    app-startup.ts
    import { services } from '@streamline-pulse/formkrafter-core'
    services.specSourceService = {
    fetchSpec: async (ref) => {
    const res = await fetch(`/api/forms/${ref}`)
    if (!res.ok) throw new Error(`Unknown form: ${ref}`)
    return res.json()
    },
    }
  2. Or configure the built-in one instead of writing your own.

    import { services, FetchSpecSourceService } from '@streamline-pulse/formkrafter-core'
    services.specSourceService = new FetchSpecSourceService({
    baseUrl: '/api/forms',
    credentials: 'include',
    })
  3. Render normally. <fk-form-render> expands nested forms automatically and shows a loading state while it resolves.

Your backend must validate the complete tree, so expand before validating:

server/validate.ts
import { expandSpec, validateFormData } from '@streamline-pulse/formkrafter-core'
const full = await expandSpec(spec)
const verdict = validateFormData(full, payload)

hasNestedForms(spec) tells you whether the work is needed at all, which is worth checking before paying for the expansion on a hot path:

import { hasNestedForms } from '@streamline-pulse/formkrafter-core'
const full = hasNestedForms(spec) ? await expandSpec(spec) : spec

A project by Streamline Pulse