Forms
There is no FormBuilder here. A form is derived from a state — its field tree, its validity and its error types are all consequences of that state and of the mutation it submits to, so they cannot drift apart from them.
Use it when you collect input that needs validation and a typed submission. Not when a single input maps to a single state — a plain state with a set is enough.
Start with the guided version
Learn step 8 builds a small form end to end before you dig into the individual insertions.
Why it is shaped this way
Three pillars, all of which follow from deriving rather than declaring:
- Form Insertions - Modular composition to tackle logic complexity
- Type-safe errors - Synchronous and asynchronous validation with type-safe exceptions (inferred from validators and submit handler)
- Parallel Forms - Support for multiple forms in the same state with automatic scoping
All of this is possible because the logic is entirely derived from the state.
Form Insertions
Form insertions enable modular composition of functionality:
insertForm
The primary insertion that creates an Angular Signal-Form from a primitive.
import { craftUse, state } from '@craft-ng/core';
import {
insertForm,
insertFormAttributes,
insertNoopTypingAnchor,
insertSelectFormTree,
cRequired,
cEmail,
} from '@craft-ng/core';
const userFormState = craftUse(
state(
'userFormState',
{ name: '', email: '' },
insertForm(
insertSelectFormTree(
'name',
insertNoopTypingAnchor, // TS limitation
insertFormAttributes(() => ({
validators: [cRequired()],
})),
),
insertSelectFormTree(
'email',
insertNoopTypingAnchor, // TS limitation
insertFormAttributes(() => ({
validators: [cRequired(), cEmail()],
})),
),
),
),
);
const form = userFormState.form;
const nameField = form.selectName();
const emailField = form.selectEmail();Note: It only works with the
stateprimitive from now.
insertNoopTypingAnchoris a special insertion that does not add any logic but allows to anchor the typing of the form field. It is required for the form system to infer the correct types of fields and exceptions. (TS limitations...)
insertFormAttributes
Adds attributes and validators to a form field.
const formState = craftUse(
state(
'formState',
{ email: '' },
insertForm(
insertSelectFormTree(
'email',
insertNoopTypingAnchor,
insertFormAttributes(() => ({
validators: [cRequired(), cEmail()],
disable: () => isLoading(),
hidden: () => !showField(),
})),
),
),
),
);
// Access email field and its exceptions
const form = formState.form;
const emailField = form.selectEmail();
const errors = emailField()().exceptions.list; // fully typed list of exceptions
const emailError = emailField()().exceptions.byValidator['cEmail'];insertFormSchema
Adds a form-level StandardSchemaV1 validator. Issues are projected onto the matching fields by their schema path, while root and unmaterialized issues stay available through schemaExceptions().
const formState = craftUse(
state(
'formState',
{ email: '' },
insertForm(
insertFormSchema(userSchema),
insertFormSubmit(saveUser),
),
),
);
const form = formState.form;
form.email.errors();
form.hasSchemaExceptions();
form.schemaExceptions();The form keeps the schema input value. Schema transformations belong at the submit boundary, for example through the mutation's methodSchema.
insertFormSubmit
The pages
- Validation — built-in, custom and async validators
- Submitting — wiring a form to a mutation, typed submit exceptions
- Nested forms — sub-trees and sub-form fields
- Exception handling — reading and shaping form errors
- Complete examples — two forms end to end
See Also
- Validators
- Submitting
- Learn step 8 — a form built end to end