Angular 21.2 brings a substantial set of improvements, with the headline being the maturation of Signal Forms. Alongside those updates, the release also delivers enhancements to resource management and developer tooling. Per the release schedule, this is the final minor version in the 21.x line, with the major release of Angular 22 expected in May. While a minor version typically limits itself to smaller features and fixes, this particular update carries considerable weight.
Angular 21.2 has arrived. As planned, this stands as the last minor release for the 21.x series. The focus now shifts to the upcoming major release, Angular 22, slated for May. The standard expectations for a minor release are limited to small features and bug fixes, yet this version is packed with changes.
Form Enhancements with Signals
The Signal Forms API has gained several practical features, rounding out its capabilities and indicating a move towards a more stable or developer-ready status.
Streamlined Form Submission
Handling form submission now requires less manual wiring. By adding the formRoot directive to your form, the default browser submit action, such as a full page reload, is automatically suppressed.
The submit handler itself is configured through the submission option within the form() function. This handler is only triggered when the user actively submits the form and the form is currently valid.
By default, submission can occur even if asynchronous validators are still in progress. During such a validation phase, the form's status remains as {invalid: false, valid: false}.
Given that submission is only blocked when invalid is true, a pending asynchronous check does not prevent the submit action.
To alter this permissive behavior, set ignoreValidators: 'none', which forces the submission to wait for all validators, including async ones, to complete.
The default is configured to be lenient. The logic is that the backend will perform its own validation upon submission, so waiting for client-side async checks before allowing the request is often unnecessary.
import { bootstrapApplication } from '@angular/platform-browser';
import {
Component, computed, inject, Injectable, signal
} from '@angular/core';
import {
form,
FormField,
FormRoot,
submit,
validateAsync,
validateStandardSchema,
} from '@angular/forms/signals';
@Injectable({ providedIn: 'root' })
export class UserService {
save(user: { firstName: string; lastName: string }) {
return Promise.resolve(undefined);
}
}
@Component({
selector: 'app-root',
imports: [FormField, FormRoot],
template: `
<form [formRoot]="userForm">
<input [formField]="userForm.firstName" />
<input [formField]="userForm.lastName" />
</form>
`,
})
export class App {
private readonly user = signal({
firstName: '',
lastName: '',
});
protected readonly userForm = form(this.user, {
submission: {
action: () => this.userService.save(this.user()),
},
});
protected readonly userService = inject(UserService);
}
bootstrapApplication(App);
Building Custom Controls
For those creating custom controls, the new transformedValue utility provides necessary tools for formatting and parsing values.
This proves especially useful for handling inputs with non-standard number or date formats.
import { bootstrapApplication } from '@angular/platform-browser';
import { Component, model, signal } from '@angular/core';
import {
form,
FormField,
FormRoot,
FormValueControl,
transformedValue,
} from '@angular/forms/signals';
import { JsonPipe } from '@angular/common';
@Component({
selector: 'app-german-number-field',
template: `<p><ng-content /></p>
<input
matInput
[value]="rawValue()"
(input)="rawValue.set($event.target.value)"
/>`,
imports: [],
})
export class GermanNumberField implements FormValueControl<number> {
readonly value = model.required<number>();
readonly rawValue = transformedValue(this.value, {
parse: (value: string) => ({ value: Number(value.replace(',', '.')) }),
format: (value) => String(value).replace('.', ','),
});
}
@Component({
selector: 'app-root',
imports: [FormField, FormRoot, GermanNumberField, JsonPipe],
template: `
<form [formRoot]="userForm">
<app-german-number-field [formField]="userForm.heightInMeter">Height in Meters</app-german-number-field>
{{user() | json}}
</form>
`,
})
export class App {
protected readonly user = signal({
heightInMeter: 1.75,
});
protected readonly userForm = form(this.user);
}
bootstrapApplication(App);
Bridging with SignalFormControl
While the new submission and custom control features are excellent, the priorities often lie in integrating these improvements into existing Angular applications.
For that purpose, the most significant addition is SignalFormControl. This allows a standard Reactive Forms FormControl to be integrated, enabling it to utilize advanced validation features from Signal Forms. This significantly eases the migration path towards Signal Forms.
import { bootstrapApplication } from '@angular/platform-browser';
import { Component, inject, resource } from '@angular/core';
import { required, debounce, validateAsync } from '@angular/forms/signals';
import { SignalFormControl } from '@angular/forms/signals/compat';
import { FormBuilder, ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-root',
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="userForm">
<input formControlName="email" />
</form>
`,
})
export class App {
protected readonly userForm = inject(FormBuilder).nonNullable.group({
email: new SignalFormControl<string>('user@host.com', (path) => {
required(path, { message: 'Email is reqired' });
debounce(path, 500);
validateAsync(path, {
params: (ctx) => ctx.value(),
factory: (params) => {
return resource({
params,
loader: () => Promise.resolve(true),
});
},
onSuccess: () => undefined,
onError: () => ({ kind: 'networkError' }),
});
}),
});
}
bootstrapApplication(App);
ResourceSnapshot is Here
The ResourceSnapshot API has finally been made available. This feature was initially anticipated for Angular 21 but was reverted just before the release.
In essence, Resource Snapshots create a blueprint for a resource, enabling you to map one resource to another or compose them for more complex tasks.
A documented example modifies the resource's behavior during loading, such that it preserves the existing value instead of reverting to undefined. Internally, the Angular team uses snapshots in testing as a convenient factory for creating resources. It will be interesting to see what other patterns emerge.
The following example demonstrates the use of a resource snapshot to define an error handler, which can prevent the control from entering the error state.
import { bootstrapApplication } from '@angular/platform-browser';
import {
Component,
computed,
Resource,
resource,
resourceFromSnapshots,
signal,
} from '@angular/core';
import { JsonPipe } from '@angular/common';
import { FormField, FormRoot, form } from '@angular/forms/signals';
function withErrorHandler<T>(
resource: Resource<T>,
errorHandler: (error: Error) => T
) {
const res = computed(() => {
const snap = resource.snapshot();
if (snap.status === 'error') {
try {
return { status: 'resolved' as const, value: errorHandler(snap.error) };
} catch (error) {
if (error instanceof Error) {
return { status: 'error' as const, error };
}
}
}
return snap;
});
return resourceFromSnapshots(res);
}
@Component({
selector: 'app-root',
imports: [FormRoot, FormField, JsonPipe],
template: `
<form [formRoot]="idForm">
<input [formField]="idForm.id" type="number"/>
<pre>{{safeUser.value() | json}}</pre>
<pre>{{unsafeUser.value() | json}}</pre>
</form>
`,
})
export class App {
idForm = form(signal({ id: 0 }));
unsafeUser = resource({
params: () => this.idForm.id().value(),
loader: ({ params: id }) => {
if (id < 0) {
throw new Error('no negative users ;)');
}
return Promise.resolve({ id, name: 'John Doe' });
},
});
safeUser = withErrorHandler(this.unsafeUser, () => ({
id: 0,
name: 'John Undefined',
}));
}
bootstrapApplication(App);
Prettier Support in the CLI
Following the precedent set by the Tailwind CSS integration, Angular 21 now treats Prettier as a supported feature within its CLI, offering built-in setup and configuration.
Introducing Lambda Expressions to Templates
The template syntax, which extends standard JavaScript, has grown by two additional constructs: lambda functions. These can now be written directly inside templates.
The primary use case is to provide an inline update function for signal modifications.
It is advised to keep business logic out of the template whenever possible, so this feature should be used sparingly and with intent.
import { bootstrapApplication } from '@angular/platform-browser';
import { Component, signal } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<p>Current Value: {{counter()}}</p>
<button (click)="counter.update((value) => value + 1)">Increment</button>
`,
})
export class App {
counter = signal(0);
}
bootstrapApplication(App);
Verifying Exhaustive Switch Statements
The @switch block now has the ability to perform an exhaustive check on its conditions. Declaring @default never in the template prompts the Angular compiler to verify that all potential values of the switch expression are handled.
import { bootstrapApplication } from '@angular/platform-browser';
import { Component, signal } from '@angular/core';
type UserType = 'admin' | 'anonymous';
@Component({
selector: 'app-root',
template: `
@switch(userType) {
@case('anonymous') {
<p>Welcome Visitor</p>
}
@case('admin') {
<p>Welcome admin</p>
}
@default never;
}
`,
})
export class App {
userType: UserType = 'admin';
}
bootstrapApplication(App);
A New Name for ChangeDetectionStrategy.Default
Acknowledging the shift that has been discussed for a while, ChangeDetectionStrategy.Default has been officially renamed to ChangeDetectionStrategy.Eager. This sets the stage for Angular 22 in May, which will change the framework’s default strategy to OnPush. The renaming disambiguates the option: since it will no longer be the default, it cannot be called "Default" and is now referred to as "Eager".
There is no immediate action required for developers. If you have explicitly set ChangeDetectionStrategy.Default, you can either remove that declaration to use the new standard or, even better, update your components to use OnPush.
These changes are optional for now. The update scripts for Angular 22 will handle the transition by applying ChangeDetectionStrategy.Eager to any components that still rely on the old ChangeDetectionStrategy.Default setting.
Sources and Acknowledgments
For the complete details, please consult the official @angular changelog. Many thanks also go out to Cedric Exbrayat.
His recent article provided the foundational information for this overview.
Merci beaucoup, Cedric!
FOREM_LTAG_END:{"tag":"open_graph","url":"https://blog.ninja-squad.com/2026/02/26/what-is-new-angular-21.2","options":"https://blog.ninja-squad.com/2026/02/26/what-is-new-angular-21.2"}
