ReactiveFormsModule FormField Setup Signal Forms Copy to clipboard import { form, FormField, required } from '@angular/forms/signals';
@Component({
imports: [FormField],
})Everything comes from @angular/forms/signals. Import the FormField directive in each component that binds inputs.
Source: Signal Forms overview (angular.dev)
FormGroup / FormControl signal() + form() Setup Signal Forms Copy to clipboard loginModel = signal({
email: '',
password: '',
});
loginForm = form(this.loginModel);Your own writable signal is the source of truth. form() builds a field tree that mirrors its shape, and types are inferred from the model.
Source: Form models (angular.dev)
FormBuilder / fb.group() form(model) Setup Signal Forms Copy to clipboard profileModel = signal({ name: '', email: '' });
profileForm = form(this.profileModel);There is no builder: a plain object in a signal replaces the group definition.
Source: Form models (angular.dev)
formControlName / [formControl] [formField] Setup Signal Forms Copy to clipboard <input type="email" [formField]="loginForm.email" />
<input type="password" [formField]="loginForm.password" />Bind each input to its field in the tree. There is no [formGroup] wrapper; use [formRoot] on a <form> only if you want submission handling (see Submission).
Source: Form models (angular.dev)
form.value / getRawValue() model() / field().value() Values Signal Forms Copy to clipboard const data = this.loginModel();
const email = this.loginForm.email().value();Calling a field (loginForm.email()) returns its FieldState; value is a writable signal on it.
Source: Form models (angular.dev)
setValue() / patchValue() model.set() / value.set() Values Signal Forms Copy to clipboard this.userModel.set({ name: 'Alice', email: 'alice@example.com' });
this.userForm.email().value.set('');
this.userForm.age().value.update((age) => age + 1);Replace the whole model with set(), or write one field through its value signal.
Source: Form models (angular.dev)
valueChanges.subscribe() computed() / effect() Values Signal Forms Copy to clipboard preview = computed(() => this.loginForm.email().value().toLowerCase());Values are signals, so derived values are computed() and side effects are effect(). No subscriptions or unsubscribing.
Source: Form models (angular.dev)
form.get('address.city') form.address.city Values Signal Forms Copy to clipboard const city = this.userForm.address.city;Fields are typed properties of the tree, so typos fail at compile time instead of returning null.
Source: Form models (angular.dev)
Validators.required / Validators.email required() / email() Validation Signal Forms Copy to clipboard loginForm = form(this.loginModel, (schemaPath) => {
required(schemaPath.email, { message: 'Email is required' });
email(schemaPath.email, { message: 'Please enter a valid email address' });
});Validation lives in the schema function passed to form(). It runs once when the form is created. Each rule can carry its own message.
Source: Validation (angular.dev)
Validators.min / max / minLength / maxLength / pattern min() / max() / minLength() / maxLength() / pattern() Validation Signal Forms Copy to clipboard min(schemaPath.age, 18, { message: 'You must be at least 18 years old' });
max(schemaPath.age, 120, { message: 'Please enter a valid age' });
minLength(schemaPath.password, 8, { message: 'Password must be at least 8 characters' });
pattern(schemaPath.phone, /^\d{3}-\d{3}-\d{4}$/, {
message: 'Phone must be in format: 555-123-4567',
});minLength() and maxLength() work on strings and arrays. min() and max() also accept a function for a reactive limit.
Source: Validation (angular.dev)
Custom ValidatorFn validate() Validation Signal Forms Copy to clipboard validate(schemaPath.website, ({ value }) => {
if (!value().startsWith('https://')) {
return { kind: 'https', message: 'URL must start with https://' };
}
return null;
});Return an error object with a kind (and optional message), or null when valid.
Source: Validation (angular.dev)
FormGroup validator (password match) validate() + valueOf() Validation Signal Forms Copy to clipboard validate(schemaPath.confirmPassword, ({ value, valueOf }) => {
if (value() !== valueOf(schemaPath.password)) {
return { kind: 'passwordMismatch', message: 'Passwords do not match' };
}
return null;
});Put the rule on the field that should show the error and read other fields with valueOf(). The error lands on confirmPassword, not the group.
Source: Validation (angular.dev)
AsyncValidatorFn validateHttp() Validation Signal Forms Copy to clipboard validateHttp(schemaPath.username, {
request: ({ value }) => `/api/check-username?username=${value()}`,
onSuccess: (response) =>
response.taken ? { kind: 'usernameTaken', message: 'Already taken' } : null,
onError: () => ({ kind: 'networkError', message: 'Could not verify' }),
});While the request runs, the field reports pending(). Combine with debounce() to avoid a request per keystroke.
Source: Validation (angular.dev)
addValidators() / removeValidators() rule option when / applyWhen() Validation Signal Forms Copy to clipboard required(schemaPath.promoCode, {
message: 'Promo code required',
when: ({ valueOf }) => valueOf(schemaPath.applyDiscount),
});
// A group of rules:
applyWhen(schemaPath, ({ valueOf }) => valueOf(schemaPath.country) === 'US', (schemaPath) => {
required(schemaPath.zipCode);
});Conditions are declared once and re-evaluate automatically; no manual updateValueAndValidity().
Source: Validation (angular.dev)
Zod / Valibot schema (manual adapter) validateStandardSchema() Validation Signal Forms Copy to clipboard import { validateStandardSchema } from '@angular/forms/signals';
import * as z from 'zod';
const userSchema = z.object({
email: z.email(),
password: z.string().min(8),
});
userForm = form(this.userModel, (schemaPath) => {
validateStandardSchema(schemaPath, userSchema);
});Works with any Standard Schema library (Zod, Valibot and others).
Source: Validation (angular.dev)
hasError() / errors in the template field().errors() State Signal Forms Copy to clipboard @if (loginForm.email().touched() && loginForm.email().invalid()) {
<ul>
@for (error of loginForm.email().errors(); track error) {
<li>{{ error.message }}</li>
}
</ul>
}errors() is an array of { kind, message } objects, so one loop renders every message.
Source: Validation (angular.dev)
valid / invalid / touched / dirty / pending valid() / invalid() / touched() / dirty() / pending() State Signal Forms Copy to clipboard this.loginForm().valid();
this.loginForm.email().touched();
this.loginForm.email().dirty();
this.loginForm().pending();Every state is a signal on the FieldState. The root form aggregates valid, invalid, pending, touched and dirty.
Source: Field state management (angular.dev)
markAllAsTouched() markAsTouched() State Signal Forms Copy to clipboard this.loginForm().markAsTouched();markAsTouched() marks the field and all its descendants. submit() also marks everything as touched for you.
Source: Field state management (angular.dev)
reset() reset() + model.set() State Signal Forms Copy to clipboard this.profileForm().reset();
this.profileModel.set({ name: '', email: '' });reset() clears touched and dirty. The data lives in your model signal, so set it back to the initial value too (reset() can also take new model data).
Source: Field state management (angular.dev)
.ng-valid / .ng-touched CSS classes provideSignalFormsConfig() State Signal Forms Copy to clipboard import { provideSignalFormsConfig } from '@angular/forms/signals';
import { NG_STATUS_CLASSES } from '@angular/forms/signals/compat';
bootstrapApplication(App, {
providers: [provideSignalFormsConfig({ classes: NG_STATUS_CLASSES })],
});Signal Forms does not add ng-* classes by default. This provider restores them so existing CSS keeps working.
Source: Migrating from Reactive Forms (angular.dev)
disable() / enable() disabled() Dynamic rules Signal Forms Copy to clipboard disabled(schemaPath.couponCode, {
when: ({ valueOf }) => valueOf(schemaPath.total) < 50,
});Declare when a field is disabled instead of toggling it imperatively. Disabled fields do not affect the parent form state.
Source: Form logic (angular.dev)
Manual *ngIf / [readonly] logic hidden() / readonly() Dynamic rules Signal Forms Copy to clipboard hidden(schemaPath.publicUrl, { when: ({ valueOf }) => !valueOf(schemaPath.isPublic) });
readonly(schemaPath.title, { when: ({ valueOf }) => valueOf(schemaPath.isLocked) });
// Template
@if (!profileForm.publicUrl().hidden()) {
<input [formField]="profileForm.publicUrl" />
}hidden() only sets state; you still decide how to render it. readonly() sets the HTML readonly attribute.
Source: Form logic (angular.dev)
valueChanges.pipe(debounceTime()) debounce() Dynamic rules Signal Forms Copy to clipboard debounce(schemaPath.query, 300);Delays model updates for that field, in milliseconds. You can also pass a function that returns a promise.
Source: Form logic (angular.dev)
Nested FormGroup / formGroupName Nested object in the model Nesting & arrays Signal Forms Copy to clipboard userModel = signal({ profile: { firstName: '' } });
userForm = form(this.userModel);
<input [formField]="userForm.profile.firstName" />The model must be made of plain objects and arrays. Nested paths bind directly; no formGroupName.
Source: Form models (angular.dev)
FormArray (push / removeAt) Array in the model + applyEach() Nesting & arrays Signal Forms Copy to clipboard orderForm = form(this.orderModel, (schemaPath) => {
applyEach(schemaPath.items, (item) => {
required(item.name);
min(item.quantity, 1);
});
});
add() {
this.orderModel.update((o) => ({ ...o, items: [...o.items, { name: '', quantity: 1 }] }));
}
// Template
@for (item of orderForm.items; track $index) {
<input [formField]="item.name" />
}Add or remove items by updating the model. applyEach() applies its rules to every item, including ones added later.
Source: Schemas and composition (angular.dev)
Shared validator arrays / helper functions schema() + apply() Nesting & arrays Signal Forms Copy to clipboard const nameSchema = schema<{ first: string; last: string }>((name) => {
required(name.first);
required(name.last);
});
userForm = form(this.userModel, (schemaPath) => {
apply(schemaPath.name, nameSchema);
});A schema bundles rules for a model shape so several forms (or array items via applyEach) can reuse it.
Source: Schemas and composition (angular.dev)
(ngSubmit) + manual valid check [formRoot] + submission action Submission Signal Forms Copy to clipboard <form [formRoot]="contactForm">...</form>
contactForm = form(this.contactModel, (schemaPath) => {
required(schemaPath.name);
}, {
submission: {
action: async (field) => {
const result = await saveContact(field().value());
if (result.ok) return;
return { kind: 'serverError', message: 'Failed to submit form' };
},
},
});Import FormRoot. The action only runs when the form is valid, and FormRoot adds novalidate to the <form> for you.
Source: Form submission (angular.dev)
Submitting from a method submit() Submission Signal Forms Copy to clipboard async onSave() {
const success = await submit(this.contactForm, async (field) => {
const result = await saveContact(field().value());
if (result.ok) return;
return { kind: 'serverError', message: 'Failed to save' };
});
if (success) {
// navigate, show confirmation, etc.
}
}submit() marks all fields as touched and resolves to true when the action completed without errors.
Source: Form submission (angular.dev)
setErrors() after a failed request Return errors from the action Submission Signal Forms Copy to clipboard action: async (field) => {
const result = await saveContact(field().value());
if (result.ok) return;
return { kind: 'serverError', message: 'Failed to submit form' };
},Return an error (or a list of errors) from the action. Add a fieldTree property to attach an error to a specific field.
Source: Form submission (angular.dev)
isSaving flag submitting() Submission Signal Forms Copy to clipboard <button type="submit" [disabled]="contactForm().submitting()">
@if (contactForm().submitting()) { Sending... } @else { Send }
</button>submitting() is true while the submission action is running.
Source: Form submission (angular.dev)
ControlValueAccessor FormValueControl Custom controls Signal Forms Copy to clipboard @Component({ /* ... */ })
export class RatingInput implements FormValueControl<number> {
value = model(0);
}
// Usage
<app-rating-input [formField]="reviewForm.rating" />Implement FormValueControl and expose a value model signal. [formField] wires up value, validation and state; no provider or callbacks.
Source: Custom controls (angular.dev)
Checkbox ControlValueAccessor FormCheckboxControl Custom controls Signal Forms Copy to clipboard export class ToggleSwitch implements FormCheckboxControl {
checked = model<boolean>(false);
}Checkbox-style controls expose a checked model signal instead of value.
Source: Custom controls (angular.dev)
Keep an existing FormControl compatForm() Interop Signal Forms Copy to clipboard import { compatForm } from '@angular/forms/signals/compat';
const user = signal({
email: '',
password: passwordControl, // existing FormControl
});
const f = compatForm(user);Top-down migration: move the form to Signal Forms while keeping controls whose validators or RxJS logic are not ported yet.
Source: Migrating from Reactive Forms (angular.dev)
Replace one control inside a FormGroup SignalFormControl Interop Signal Forms Copy to clipboard import { SignalFormControl } from '@angular/forms/signals/compat';
emailControl = new SignalFormControl('', (p) => {
required(p, { message: 'Email is required' });
});
form = new FormGroup({ email: this.emailControl });
<input [formField]="emailControl.fieldTree" />Bottom-up migration: convert leaf controls first while the parent FormGroup stays. Values sync both ways.
Source: Migrating from Reactive Forms (angular.dev)