Guides
Form specs
Everything in FormKrafter starts here. A form is a spec: a plain JSON tree that you can store in a database, diff in a pull request, send over the wire and hand to any of the renderers, web or native. Nothing about it is framework-specific, and nothing about it needs FormKrafter to be readable.
The BrickSpec shape
Section titled “The BrickSpec shape”Every node in the tree — a field, a panel, a wizard step — is a BrickSpec.
Only three properties are required:
interface BrickSpec { type: 'panel' | 'input' | 'collection' | 'output' | 'action' // required id: string // which registered brick renders this node name: string // human label, shown in the builder
dataType?: 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | 'null' | 'void' configs?: { key?: string } & Record<string, unknown> styles?: Record<string, unknown> validations?: Validation[] rules?: Rule[] children?: BrickSpec[]
category?: string editable?: boolean isPrivate?: boolean}The property you touch most lives inside configs: key, the name of the
field in the form data. It is what ends up in your payload, and a brick without
a key holds no data.
A complete spec
Section titled “A complete spec”A form with a text field and a select, both required:
{ "type": "panel", "id": "column", "name": "Contact", "configs": { "key": "contact" }, "children": [ { "type": "input", "dataType": "string", "id": "text", "name": "Text", "configs": { "key": "fullName", "label": "Full name" }, "validations": [{ "validator": "required" }] }, { "type": "input", "dataType": "string", "id": "select", "name": "Select", "configs": { "key": "role", "label": "Role", "optionsSource": "static", "options": "Developer\nDesigner\nManager" }, "validations": [{ "validator": "required" }] } ]}That spec produces this data:
{ "fullName": "Ada Lovelace", "role": "Developer" }Form data is flat — one entry per key, regardless of how deeply the brick
is nested in panels, wizard steps or tabs. The single exception is a
collection brick (like data-grid), where each row is its own scoped record:
{ "fullName": "Ada", "contacts": [{ "email": "a@b.c" }, { "email": "d@e.f" }] }Editing a spec
Section titled “Editing a spec”Never mutate a spec in place. Every operation returns a new spec plus the patches to get there and back:
import { moveBrick, addBrick, SpecHistory } from '@streamline-pulse/formkrafter-core'
const update = moveBrick(spec, '0.0', '0.2.0')// update.spec → the new spec// update.patches → RFC 6902 operations that applied the change// update.inverse → RFC 6902 operations that undo itPaths are dot-strings rooted at "0", using pre-removal coordinates: "0.2.0"
is the first child of the third child of the root.
Available operations
Section titled “Available operations”| Operation | Signature | Merge behavior |
|---|---|---|
addBrick |
(spec, brick, parentPath, index?) |
— |
removeBrick |
(spec, path) |
throws on the root path "0" |
moveBrick |
(spec, from, to) |
throws when moving a brick into its own subtree |
duplicateBrick |
(spec, path) |
— |
updateBrickConfigs |
(spec, path, configs) |
merged into existing configs |
updateBrickStyles |
(spec, path, styles) |
merged |
updateBrickValidations |
(spec, path, validations) |
replaced wholesale |
updateBrickRules |
(spec, path, rules) |
replaced wholesale |
Every operation addresses bricks the same way, by path. A path that does not resolve to a brick throws rather than falling back to the root, so a typo can never edit the wrong node.
Undo and redo
Section titled “Undo and redo”SpecHistory records updates and replays their inverses:
-
Record every update as it happens.
const history = new SpecHistory()history.record(update) -
Check whether stepping is possible — useful for disabling toolbar buttons.
history.canUndo // booleanhistory.canRedo // boolean -
Step back or forward, passing the current spec.
spec = history.undo(spec) ?? specspec = history.redo(spec) ?? spec
The builder does exactly this internally, which is why its specChange event
carries { spec, patches, inverse } — you can persist the patches, replay them
on a server, or drive your own history stack.
Walking a spec
Section titled “Walking a spec”import { iterateBricks, getBrickAt, pointerFromPath } from '@streamline-pulse/formkrafter-core'
for (const { brick, path } of iterateBricks(spec)) { console.log(path, brick.configs?.key)}
getBrickAt(spec, '0.1') // the brick at a dot-path, or undefinedpointerFromPath('0.1') // the matching JSON PointerNext steps
Section titled “Next steps”A project by Streamline Pulse