Core Concepts
Data Model
One of the most important concepts in the Angular Signal Forms is the Data Model. Everything starts from there. When we create a Signal Form, it uses the Data Model as the source of truth for the values of the form fields. There is no internal copy of the data maintained from the Signals Forms library. That means that updating the Data Model will update automatically a form field's value. In the same way, changing the value of form field, will directly modify the Data Model.
Also, when creating a Signal Form, the resulting form structure (FieldTree) will match the shape of the Data Model.

If the
form()factory is not defined inside an injection context (e.g contructor), an injector can be passed in the form options object.form(signal({username: ''}, {injector: this.injector}));.
Binding form fields to UI elements
After defining the data model and generating the form field tree, the next step is to bind each field to its corresponding UI element. Angular provides the Field directive to accomplish this.
A UI element can be one of the three:
| UI element | Description |
|---|---|
| Native HTML form element | A standard browser form control |
| Signal forms custom control | A custom form control built using Angular signal-based forms |
ControlValueAccessor component | A component implementing CVA for Angular Reactive/Template-driven forms |
This directive will:
- Create a two way binding between the native UI element's value and the form field's value.
- Synchronize the field state (disabled, touched, required, etc.) with the native UI element.
Form Inspector
Field Tree
(root) valid: true | invalid: false | pending: false
value: {
"firstName": "Stef",
"lastName": "Modify me",
"address": {
"street": "123 Main St",
"city": "Anytown"
}
}disabledReasons: (0)
[]errors: (0)
[]errorSummary: (0)
[]meta: {
"pattern": [],
"required": false
}
Children
firstName valid: true | invalid: false | pending: false
value: "Stef"disabledReasons: (0)
[]errors: (0)
[]errorSummary: (0)
[]meta: {
"pattern": [],
"required": false
}
Children
— none —
lastName valid: true | invalid: false | pending: false
value: "Modify me"disabledReasons: (0)
[]errors: (0)
[]errorSummary: (0)
[]meta: {
"pattern": [],
"required": false
}
Children
— none —
address valid: true | invalid: false | pending: false
value: {
"street": "123 Main St",
"city": "Anytown"
}disabledReasons: (0)
[]errors: (0)
[]errorSummary: (0)
[]meta: {
"pattern": [],
"required": false
}
Children
street valid: true | invalid: false | pending: false
value: "123 Main St"disabledReasons: (0)
[]errors: (0)
[]errorSummary: (0)
[]meta: {
"pattern": [],
"required": false
}
Children
— none —
city valid: true | invalid: false | pending: false
value: "Anytown"disabledReasons: (0)
[]errors: (0)
[]errorSummary: (0)
[]meta: {
"pattern": [],
"required": false
}
Children
— none —
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { Field, form } from '@angular/forms/signals';
import { FormInspectorComponent } from '../../../ui/form-inspector.ts/form-inspector';
import { DemoLayout } from '../../../ui/demo-layout/demo-layout';
interface MyDataModel {
firstName: string;
lastName: string;
address: {
street: string;
city: string;
};
}
@Component({
selector: 'field-tree',
templateUrl: './field-tree.html',
imports: [Field, FormInspectorComponent, DemoLayout],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FieldTree {
protected readonly myForm = form(
signal<MyDataModel>({
firstName: 'Stef',
lastName: 'Modify me',
address: { street: '123 Main St', city: 'Anytown' },
}),
);
}Field Logic
Now that every field node is synchronized with the corresponding UI element using the [field] directive, we can add logic to it. We can add the follwing types of logic in our form fields.
| Type of logic | Description |
|---|---|
| hide fields | Logic that determines if the field is hidden. |
| disable fields | Logic that determines reasons for the field being disabled. |
| readonly fields | Logic that determines if the field is read-only. |
| validation errors | Logic that produces synchronous validation errors for the field. |
| validation errors (field's subtree) | Logic that produces synchronous validation errors for the field's subtree. |
| asynchronous validation errros | Logic that produces asynchronous validation results (errors or 'pending'). |
To add logic like this to our form we need to use a Schema. In the Schema we declaretively define the logic of our form. You can think the Schema as a function that accepts a FieldPath and defines the logic for it.
FieldPath<T>is an object that represents a location in theFieldTreeform structure and is used to bind logic to a particular part of the structure prior to the creation of the form.

We explore the
Field State
Based on the Field Logic we have defined using the Schema, we get the Field State. To access the state associated with a field, call it as a function as you can see in the following demo:
Form Inspector
Field Tree
(root) valid: true | invalid: false | pending: false
value: {
"firstName": "Delete me",
"lastName": "Disabled"
}disabledReasons: (0)
[]errors: (0)
[]errorSummary: (0)
[]meta: {
"pattern": [],
"required": false
}
Children
firstName valid: true | invalid: false | pending: false
value: "Delete me"disabledReasons: (0)
[]errors: (0)
[]errorSummary: (0)
[]meta: {
"pattern": [],
"required": true
}
Children
— none —
lastName valid: true | invalid: false | pending: false
value: "Disabled"disabledReasons: (1)
[
{
"message": "Last name cannot be changed"
}
]errors: (0)
[]errorSummary: (0)
[]meta: {
"pattern": [],
"required": false
}
Children
— none —
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { disabled, Field, form, required, schema } from '@angular/forms/signals';
import { FormInspectorComponent } from '../../../ui/form-inspector.ts/form-inspector';
import { DemoLayout } from '../../../ui/demo-layout/demo-layout';
interface MyDataModel {
firstName: string;
lastName: string;
}
@Component({
selector: 'field-logic',
templateUrl: './field-logic.html',
imports: [Field, FormInspectorComponent, DemoLayout],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FieldLogic {
protected readonly myForm = form(
signal<MyDataModel>({
firstName: 'Delete me',
lastName: 'Disabled',
}),
schema<MyDataModel>((schemaPath) => {
required(schemaPath.firstName);
disabled(schemaPath.lastName, 'Last name cannot be changed');
}),
);
}The Field State includes the following state which is derived from the Field Logic. Here is a reference of all the available Field states we can get:
| Field State | Description |
|---|---|
errors | A signal containing the current errors for the field. |
invalid | A signal indicating whether the field is valid. |
disabled | A signal indicating whether the field is currently disabled. |
disabledReasons | A signal containing the reasons why the field is currently disabled. |
max | A signal indicating the field's maximum value, if applicable (numeric/date inputs & custom controls). |
maxLength | A signal indicating the field's maximum string length, if applicable (<input>, <textarea>, custom controls). |
min | A signal indicating the field's minimum value, if applicable (numeric/date inputs & custom controls). |
minLength | A signal indicating the field's minimum string length, if applicable (<input>, <textarea>, custom controls). |
name | A signal containing the field's unique name, typically based on its parent field name. |
pattern | A signal indicating patterns the field must match (array of RegExp). |
readonly | A signal indicating whether the field is currently readonly. |
required | A signal indicating whether the field is required. |
touched | A signal indicating whether the field has been touched by the user. |
value | A writable signal containing the field’s value; updates sync with the bound data model. |