Skip to main content

1. Declare the model

Start with a component-owned form tree. field() creates leaf values and form() infers their combined object shape.

import { Component } from '@angular/core';
import { field, FormNodeDirective, form } from '@ngblocks/form-nodes';

@Component({
selector: 'app-profile-editor',
imports: [FormNodeDirective],
template: `
<label>
Name
<input [formNode]="form.name" />
</label>

<p>Current name: {{ form.name() }}</p>
`,
})
export class ProfileEditor {
form = form({
name: field(''),
age: field<number>(null),
email: field(''),
});
}

Fields infer nullability from their initial value. Use field.nullable('') when null is allowed; this adds null to the inferred string type. See Field nullability.

For the declaration above, the inferred value is equivalent to:

type ProfileValue = {
name: string;
age: number | null;
email: string;
};

You do not need to maintain that interface separately. TypeScript derives it from the declaration.

Let the form declaration infer the value

Start from fields and let form() derive the aggregate type. Add an explicit domain type only where it communicates a boundary or constrains a nullable/union value more precisely.

📝 Read and update values

Call nodes directly to read their committed values:

this.form(); // { name: '', age: null, email: '' }
this.form.name(); // ''
this.form.age(); // null

Use methods directly on fields and forms:

this.form.name.set('Ada');
this.form.age.update(age => (age ?? 0) + 1);

this.form.patch({
email: 'ada@example.com',
});

The model itself also works outside Angular and does not require dependency injection. The first field is already connected to a native input; next, bind the remaining fields and examine the control interaction behavior.

Continue with Step 2: Bind controls.