What's new in Angular 22.0?
Angular 22.0.0 is here!
This release continues the modernization work started in the previous versions: Signal Forms and resources are now stable, OnPush is now the default change detection strategy for components, and the HTTP client now uses Fetch by default.
There are also a new decorator, and quite a few additions around AI tooling.
Let's dive in!
TypeScript v6 and Node v22 required
Angular v22 now requires TypeScript v6. Older versions of TypeScript, including v5.9, are not supported anymore.
Angular v22 also drops support for Node v20, and Node v26 is supported.
OnPush by default
Angular offers two change detection strategies:
Eager(which was previously namedDefault),OnPush.
Until Angular v21, the default strategy was Eager.
Since Angular v22, the default strategy is now OnPush!
This means that a component that does not explicitly specify a change detection strategy
will now use OnPush instead of Eager.
Of course, there is a migration available to automatically add changeDetection: ChangeDetectionStrategy.Eager
to all components that do not specify a strategy yet,
so that they keep using the Eager strategy when you upgrade to Angular v22.
It also replaces Default by Eager in the component decorators if needed.
If a component is already using OnPush,
then the migration keeps the changeDetection property from the component decorator,
even if it is not needed anymore.
Signal Forms are stable!
One of the biggest changes in Angular v22 is that the signal forms APIs are no longer experimental! They graduated to stable and are now available for production use.
👉 To learn more about signal forms, check out our dedicated articles.
A few changes landed before the stabilization though, so let's start with the breaking ones before going into the new features.
touch/touched
Custom form components used to be able to bind to the touched model,
but after reflecting on the API, the Angular team realized that this was not the best approach,
as it was allowing a control to mark the field as no longer touched.
Instead, we now have a touched input to know if the field is touched or not,
and a touch() output to mark the field as touched.
You'll have to manually make this change in your codebase if you were using the touched model before.
markAsTouched()
markAsTouched() now marks a field and all its descendants as touched, instead of only the field itself.
This behavior can be overridden by passing { skipDescendants: true } as an argument to the method.
Consistent when
All validators and dynamic behavior functions (disabled, readonly, hidden, etc.),
now have a when option to specify when they should be applied,
instead of passing the reactive function directly as an argument.
The old signature is still supported for backward compatibility, but is deprecated:
// before
disabled(form.age, ({ valueOf }) => valueOf(form.isAdmin));
// 👇after
disabled(form.age, { when: ({ valueOf }) => valueOf(form.isAdmin) });
minDate() and maxDate() validators
Two new validators minDate() and maxDate() were added,
allowing you to easily validate date inputs:
protected readonly userForm = form(
model,
form => {
// ...
// 👇date should be between January 1st, 1900 and today
minDate(form.birthDate, new Date('1900-01-01'));
maxDate(form.birthDate, new Date())
}
);
This adds minDate and maxDate errors to the field if the date is out of range.
debounce a field on blur
Signal forms already supported debouncing the value changes of a field,
by using the debounce function and specifying a delay in milliseconds or a function that returns a promise for fine-grained control.
It is now also possible to debounce the value changes on blur, by using the 'blur' option:
form => {
// ...
// 👇debounce the password field on blur
debounce(form.password, 'blur')
}
debounce async validators
As you sometimes want to debounce only the asynchronous validators of a field,
we now have a debounce option in validateAsync() and validateHttp():
validateHttp(form.email, {
// 👇 httpResource is triggered when the email signal changes
request: email => `/api/users/check?email=${email.value()}`,
// 👇 specify a debounce duration to avoid sending a request on every keystroke
debounce: 400,
// ...
});
reloadValidation()
Since we are talking about asynchronous validators,
a new reloadValidation() method was added to allow you to easily re-run the asynchronous validators of a field.
When called on a field, it will re-run the asynchronous validators of this field and all its descendants,
by calling the reload() method of the underlying resources.
This matches what we could do in legacy forms with updateValueAndValidity().
getError()
Another method has been added to a field: getError().
It allows you to easily get the error of a specific validator, by passing the error kind as an argument,
and removes the need to iterate over the errors object of the field to find the error you are looking for:
<input id="login" class="form-control" [formField]="userForm.login" />
@let login = userForm.login();
@if (login.touched() && login.invalid()) {
@if (login.getError('required')) {
<div>Login is required</div>
}
<!-- 👇 getError() narrows the type of the error to minLength error -->
@if (login.getError('minLength'); as minLengthError) {
<div>Login should be {{ minLengthError.minLength }} characters</div>
}
}
ControlValueAccessor compatibility
Custom form components have been possible in Angular since the very beginning, by implementing the ControlValueAccessor interface.
In Signal Forms, we now have the new FormControlValue interface,
which is more powerful and easier to use than ControlValueAccessor.
To simplify the migration to signal forms,
the Angular team made legacy custom form components based on ControlValueAccessor compatible with signal forms.
One thing was missing though: ControlValueAccessor components were able to define validators inside themselves
(using the NG_VALIDATORS provider),
but there was no way to use the errors added by these validators in signal forms.
This is now fixed, as these validation errors are propagated to signal forms fields!
FormControlValue compatibility
FormControlValue is now fully compatible with legacy forms,
both reactive and template-driven.
This means you can migrate your custom form components currently using ControlValueAccessor
to use FormControlValue instead,
without having to change the way you use them.
Let's say Rating is a custom form component based on FormControlValue.
The following examples will work without any change after the migration to Angular v22:
<!-- legacy reactive form -->
<ns-rating formControlName="rating"></ns-rating>
<!-- legacy template-driven form -->
<ns-rating [(ngModel)]="rating"></ns-rating>
FormControlValue also gained a reset method that you can implement in your custom form components
to support resetting the field as you want when the form field is reset.
That's all for forms, now we can go rewrite the thousands of forms in our codebase to use these new APIs! 😅
Debouncing signals
Forms have a few options to debounce the value changes of a field,
but what if you want to debounce any signal in your application?
Angular v22 now has a new (experimental) debounced() function that you can use.
It creates a debounced version of a signal, but it does not return another signal.
It returns a Resource, because a debounced value has a state:
it can be settled, waiting for a pending value, or in an error state if the source signal throws
(a Resource is the read-only version of the ResourceRef returned by the resource() function).
For example, if we want to debounce a search query, we can write:
@Component({
imports: [FormField],
template: `
<input type="search" placeholder="Filter users" [formField]="queryForm" />
<p>Debounced query: {{ debouncedQuery.value() }}</p>
@if (debouncedQuery.isLoading()) {
<p role="status">Waiting for stable query...</p>
}
@if (filteredUsers.hasValue()) {
<ul>
@for (user of filteredUsers.value(); track user.id) {
<li>{{ user.name }}</li>
}
</ul>
}
`
})
class SearchComponent {
protected readonly query = signal('');
protected readonly queryForm = form(this.query);
// 👇 create a debounced version of the `query` signal
protected readonly debouncedQuery = debounced(() => this.query(), 300);
protected readonly filteredUsers = inject(UserService).search(this.debouncedQuery.value);
}
Here debouncedQuery.value() initially returns the current value of query.
When query changes, debouncedQuery switches to a loading state,
but its value still stays the previous settled value.
If no new change happens during the next 300 milliseconds,
the resource resolves with the new value.
This makes it a good fit when you want to drive another asynchronous operation from a stabilized value.
For example, a resource or an HTTP resource can use debouncedQuery.value() as its request parameter,
and the template can use debouncedQuery.isLoading() to display that the user input has not settled yet.
The second parameter can also be a function instead of a number.
In that case, the function receives the new value and the previous resource snapshot,
and must return a Promise that resolves when the new value should be published.
If you followed closely, there are now three variations of debouncing in Angular:
- you can debounce a form field value on input with
debounce(field, delay) - you can debounce an asynchronous validator with
validateHttp(field, { debounce: delay }) - or you can debounce any signal value with
debounced(signal, delay)
Resources are stable!
resource(), rxResource(), and httpResource() are now stable and ready to use in production as well!
This changes the recommended way to handle asynchronous data in Angular applications,
and we encourage you to give them a try if you haven't already,
as they provide a really nice developer experience.
Before stabilizing resources, the Angular team made a few improvements.
It is now easier to chain resources, thanks to the chain() method exposed in the params of the request function:
const userResource = httpResource(() => `/api/users/${id()}`);
const postsResource = httpResource({
params: ({ chain }) => {
const user = chain(userResource);
return { authorId: user.authorId };
},
loader: ({ authorId }) => fetchPostsByUserId(authorId)
});
chain(userResource) either returns userResource.value() when available,
or automatically propagates the userResource state into postsResource:
- if
userResourceis idle,postsResourcebecomes idle; - if
userResourceis loading/reloading,postsResourceswitches to loading; - if
userResourceerrors,postsResourceerrors with aResourceDependencyError; - if
userResourceis resolved,postsResourceloads normally.
Improvements have also been made for SSR,
as resource and rxResource can now be cached by specifying an id in the options.
A cached resource will be immediately displayed without going through the loading state
on the client after being rendered on the server.
New @Service decorator
It's been a while since we had a new decorator in Angular,
but Angular v22 introduces the @Service decorator to define services in a more concise way.
It is basically the same as @Injectable({ providedIn: 'root' }),
a shorter way to declare a service
that should be provided in the root injector,
with a nice name that makes it clear that this class is a service.
@Service()
class UserService {
// ...
}
The limitation is that this service must use the inject function
to inject its own dependencies, rather than constructor injection.
If you don't want to provide the service in the root injector,
you can use the autoProvided: false option of the @Service decorator,
and then provide the service in another fashion.
It is now recommended to use @Service() for services,
and this is what the CLI generates with ng generate service by default in v22.
You can still generate services with @Injectable()
if you want to specify a different provider scope or other options,
by using ng generate service --injectable.
There is no automatic migration available for this change (yet?).
injectAsync()
You all know the inject() function that allows you to inject a dependency.
Angular v22 now also has an injectAsync() function.
It lets you lazy-load a service only when you actually need it,
while still creating the service instance with Angular dependency injection.
This can be useful for heavy services that are only needed after a user action,
for example a reporting service used when exporting a PDF.
The service must be auto-provided, either with @Service() or with @Injectable({ providedIn: 'root' }).
Then, instead of injecting it eagerly with inject(ReportService),
you can use injectAsync with a dynamic import:
export class Admin {
private readonly reportService = injectAsync(() => import('./report.service').then(m => m.ReportService));
async exportPdf() {
const reportService = await this.reportService();
await reportService.exportPdf();
}
}
You still need to call injectAsync from an injection context, for example in a component or service field initializer, just like inject.
You can also ask Angular to prefetch the service before it is requested.
For example, onIdle starts loading the service when the browser is idle:
export class AdminWithPrefetch {
private reportService = injectAsync(() => import('./report.service').then(m => m.ReportService), {
prefetch: onIdle
});
async exportPdf() {
const reportService = await this.reportService();
await reportService.exportPdf();
}
}
In that case, the bundle can be downloaded in the background after the application starts,
but the service is still only used by the component when this.reportService() is called.
Note that if the service uses a default export,
you can directly pass the dynamic import to injectAsync without the need for a then:
injectAsync(() => import('./report.service')).
Http
The HTTP client now uses the Fetch API under the hood instead of XMLHttpRequest.
It was already recommended to use withFetch() when using SSR,
for better performance and compatibility.
withFetch() is now the default, and is marked as deprecated, so you can remove it from your codebase if you were using it before.
A migration will automatically remove withFetch() from your codebase if you were using it,
and add withXhr() if not, to keep using the old XMLHttpRequest implementation,
as it is technically a breaking change.
Note that if you removed provideHttpClient() from your codebase
(as it is no longer necessary since Angular v21),
the migration will do nothing.
It should be fairly transparent if you choose to switch over to the new Fetch API:
the only thing unsupported by the Fetch implementation is the upload progress report.
To add meaningful warnings, the reportProgress option is now deprecated,
and reportUploadProgress and reportDownloadProgress must now be used instead.
Router
The router has an option called withComponentInputBinding()
since Angular v16
that offers the possibility to bind parameters as inputs.
The function now accepts configuration options.
The first option is queryParams,
a boolean that lets you specify whether query parameters should be bound as inputs or not (default: true).
The second option is unmatchedInputBehavior
to specify what should happen when an input does not match any parameter.
The possible values are:
'alwaysUndefined': always bindsundefinedto unmatched inputs (default);'undefinedIfStale': bindsundefinedonly if the input was previously available in the router data for the active route. This avoids settingundefinedfor inputs that were never expected to be set by the router.
The RouterLink directive gained a new input browserUrl
that allows you to specify the URL to update in the browser when the link is clicked
(this option was introduced in Angular v18).
You can now write <a [routerLink]="['/users', user.id]" browserUrl="/my-profile">Profile</a>
to navigate to /users/:id when the link is clicked, but update the browser URL to /my-profile.
To conclude the router section, note that this release has two breaking changes:
canMatchfunctions now have a mandatory third parametercurrentSnapshot. An automatic migration is provided.- the
paramsInheritanceStrategyoption is now set to'always'by default, ensuring that route parameters are inherited from parent routes by default. You'll need to explicitly set it to'emptyOnly'if you want to keep the previous behavior of only inheriting parameters when the child route does not have any parameter of its own, as there is no migration for this change.
Templates
The compiler received a few changes as well.
strictTemplates by default
strictTemplates is now enabled by default,
so you no longer need to specify it in your tsconfig.json to get the benefits of strict template type checking.
A migration adds strictTemplates: false to your tsconfig.json if it is not already enabled,
to keep the previous behavior when you upgrade.
Comments inside elements
It is now possible to add comments inside HTML elements:
<div /* comment */>...</div>
<div
// comment
>...</div>
@default never
Angular v21.2
added the possibility to use @default never in switch statements to ensure that all cases are handled.
The feature was limited, as it did not work on nested properties.
It is now possible to use it with any type of switch statement, even if the discriminant is a nested property access,
by specifying the type of the discriminant with @default never(entity).
For example:
@let data = chartData();
@switch (data.type) {
@case ('line-chart') {
<line-chart [data]="data" />
}
@case ('bar-chart') {
<bar-chart [data]="data" />
}
@default never(data);
}
whereas this still works out of the box since v21.2:
@switch (status()) {
@case ('idle') {
<p>Idle</p>
}
@case ('loading') {
<p>Loading</p>
}
// no need to specify the discriminant as it is a simple value
@default never;
}
New compiler checks and errors
The compiler also has two new checks and errors. If an element is matched by multiple components in a template, then an error is thrown (this used to be caught at runtime before, but is now detected at compile time):
[ERROR] NG8023: Multiple components match node with tagname pr-menu: 'Menu', 'OtherMenu'. [plugin angular-compiler]
Another error is thrown if a component or directive exposes several inputs, outputs or models with the same name, for example via an alias:
readonly user = input('');
readonly user2 = input('', { alias: 'user' });
throws:
✘ [ERROR] NG1054: Input 'user' is bound to both 'user' and 'user2'. [plugin angular-compiler]
Another example is when a component exposes a model and an output with the same name:
readonly user = model('');
readonly userChange = output<string>();
This one is automatically fixed by the migration, by using an input and a linked signal:
readonly userInput = input('', { alias: 'user' });
readonly user = linkedSignal(this.userInput);
readonly userChange = output<string>();
Optional chaining
Another improvement is that the compiler now better supports ?. optional chaining in templates.
For example, this code used to throw an error,
as the compiler did not understand that {{ project.author }} was protected by the if guard
and thought project could be null when trying to access author:
@if (project?.author) {
{{ project.author }}
}
This is no longer the case and works properly in Angular v22.
As a common workaround before this improvement,
we had to write {{ project?.author }} instead of {{ project.author }},
and this is no longer necessary.
More than that: the extended compiler diagnostics nullishCoalescingNotNullable and optionalChainNotNullable
may now complain about such unnecessary workarounds and suggest removing them.
To avoid having to fix all these when upgrading,
a migration disables these diagnostics if they are enabled in your codebase,
but you can re-enable them after the migration to fix the potential issues.
Another change in this area is that Angular semantics now align with TypeScript for optional chaining in templates.
For example, project?.author returned null in Angular templates when project was null,
while it returned undefined in TypeScript code.
This is because Angular made this choice before TypeScript had its own semantics for optional chaining,
but now that TypeScript does,
Angular follows it and project?.author returns undefined when project is null or undefined.
As this might be fairly breaking,
a migration wraps such optional chaining expressions in a $safeNavigationMigration() function to keep the previous behavior:
// 👇 after v22 update
{{ $safeNavigationMigration(project?.author) }}
You'll then have to go over these,
and carefully check if you can remove the $safeNavigationMigration() wrapper or not.
@defer
The on idle trigger now accepts a timeout option to specify a timeout for the idle callback
(as the underlying requestIdleCallback does),
to avoid waiting indefinitely for the browser to be idle if it never happens.
The callback will be called when the browser is idle,
or when the timeout expires, whichever happens first.
@defer (on idle(500ms)) {
<!-- content to defer -->
}
We can now also customize the idle behavior of @defer with the new provideIdleServiceWith(),
to which we can pass a custom idle service that implements the IdleService interface:
@Service()
class CustomIdleService implements IdleService {
requestOnIdle(callback: (deadline?: IdleDeadline) => void): number {}
cancelOnIdle(id: number): void {}
}
The default implementation uses requestIdleCallback/cancelIdleCallback when available,
and falls back to a setTimeout/cancelTimeout when it is not.
Testing
A new TestBed.getLastFixture() method has been added to easily get the last created fixture in your tests,
without having to keep a reference to it in a variable.
beforeEach(() => TestBed.createComponent(User));
test('', () => {
// 👇 get the fixture created in the beforeEach
const fixture = TestBed.getLastFixture();
//...
});
Another notable addition is that Zone.js tests now support the fakeAsync, flush,
and waitForAsync utilities when using Vitest.
This should simplify the migration from other test frameworks,
as we were forced to rewrite these kinds of tests when migrating to Vitest before this improvement.
Well, we'll still have to rewrite them at some point to remove the dependency on Zone.js,
but at least we can do it in a second step now.
You can simply add "zone.js/plugins/vitest-patch" to the polyfill entry
of your angular.json test target to get this support.
SSR
Incremental hydration is now the default hydration strategy for server-side rendering. You can check out our article about Angular v19 to learn more about incremental hydration and how it works.
This means withIncrementalHydration() is now no longer necessary in provideClientHydration()
and has been marked as deprecated.
If you were not using it,
a new withNoIncrementalHydration() call is added by the migration to keep using the old hydration strategy.
Another notable addition is that the provideServerRendering() function
now accepts an options object as a first parameter.
A single maxResponseBodySize property is available for now to specify the maximum size of the response body in bytes
(default is 1 MB).
provideServerRendering(
{ maxResponseBodySize: 2 * 1024 * 1024 }, // 2 MB
withRoutes(serverRoutes)
);
Angular ARIA
The @angular/aria package,
introduced in Angular v21 to provide accessibility components,
is no longer in developer preview and is now generally available for production use.
It has also been updated to work seamlessly with signal forms.
AI
Well, this section keeps growing with every release, so buckle up, as we have a lot of new AI features in Angular v22!
Building apps with AI
If you are a frequent reader of our blog, you know that the CLI has had an MCP server since Angular v20.1. This MCP helps your coding agent interact with your Angular project.
But practices evolve rather quickly in the AI world (lol), and skills are all the rage nowadays. Skills are a way to teach your agent how to perform specific tasks in a specific context, and the Angular team wrote a few skills to help your agent produce better code when working on an Angular project.
To install this set of Angular skills, you can run the following command:
npx skills add https://github.com/angular/skills
This adds two skills that your coding agent can use:
angular-new-appthat contains guidelines for creating new Angular projectsangular-developerthat contains guidelines for generating Angular code and providing architectural guidance.
The second one is the most interesting, as it covers a wide range of topics and provides best practices for working with Angular, up-to-date with the latest version of the framework and all its new features. You'll see your agent loading this skill when asked to generate Angular code, and it should generate state-of-the-art code.
As the skill now takes the lead for code generation,
the MCP server no longer offers the find_examples and modernize tools.
Debugging apps with AI
When running in development, an Angular application using Angular v22 now exposes two tools to help your agent debug your application: the signal graph and the dependency injection graph, as you can see them in Angular DevTools. These tools are registered in Chrome DevTools as third-party tools. You may wonder why this is listed in the AI section then?
This is because you can then use the chrome-devtools-mcp
to connect your agent to Chrome DevTools and let it call these Angular tools.
This requires a bit of fiddling with the MCP flags to enable third-party tools,
but then your agent gets insights about the application state and how to fix it when something is wrong.
Using WebMCP in Angular
As the rest of the AI features are based on WebMCP, let me just quickly introduce it before.
WebMCP is a proposed web standard that lets a website expose Model Context Protocol capabilities directly from a web page. So instead of asking an MCP server on your machine or in the cloud, an agent can directly use a website as a tool and interact with it to perform specific tasks. This is a very early experiment (you'll need a flag to enable it in Chrome Beta at the moment), but as Google is pushing it, the framework and CLI team decided to implement some capabilities.
WebMCP has two main parts:
- it lets a website expose tools via an imperative API, and those tools can be called by an agent.
- if a website adds some attributes to its forms, then these forms are automatically detected as tools that can be called by an agent (declarative API).
Angular v22 lets you leverage these capabilities.
A new declareExperimentalWebMcpTool() function (experimental, you guessed it)
is available in @angular/core to declare a WebMCP tool imperatively in your Angular application.
So you can write this code in a component:
export class Users {
constructor() {
declareExperimentalWebMcpTool({
name: 'list_users',
description: 'List users with a specific status',
inputSchema: {
type: 'object',
properties: {
status: { type: 'string', enum: ['ADMINS', 'STUDENTS'] }
},
required: ['status'],
additionalProperties: false,
},
execute: ({ status }) => {
return inject(UserService).list(status);
}
})
}
}
As you can see, it is possible to use inject in the execute function of the tool to leverage Angular dependency injection,
and Angular destroys the tool when the component is destroyed.
Or you can declare the tool in the app providers (or in the providers of a route),
using provideExperimentalWebMcpTool():
export const appConfig: ApplicationConfig = {
providers: [provideExperimentalWebMcpTool([{
name: 'list_users',
// ...
}])]
};
The other part of the WebMCP proposal is the declarative API for forms. This normally means that a developer must add attributes to their forms to make them detectable by agents as tools:
<form toolname="createUser" tooldescription="Creates a new user">
But if you use Signal Forms, then you can automatically register a tool from your forms!
This is opt-in, by adding provideExperimentalWebMcpForms() from @angular/forms/signal to your app providers:
export const appConfig: ApplicationConfig = {
providers: [
provideExperimentalWebMcpForms(),
// ...
]
};
Then you can define the metadata of the tool in the form() function,
which now has an optional experimentalWebMcpTool key in its options:
protected readonly userForm = form(
// ...
{
experimentalWebMcpTool: {
name: 'user_creation',
description: 'Form to create a new user'
}
}
)
This will then call declareExperimentalWebMcpTool() for you with the name and description you defined,
and the input schema of the tool will be automatically generated from the form fields.
The execute function of the tool will automatically fill the form with the input data and submit it.
Angular CLI
The CLI has received a few improvements as well, especially to ease the migration to Vitest.
Unit tests
As mentioned above, some work has been done on the CLI to ease the migration to Vitest.
Zone.js can now be used with Vitest.
A new migration migrate-karma-to-vitest is available to automatically
migrate your test setup from Karma to Vitest,
adding and removing the necessary dependencies and configuration to your project.
You can then run the existing refactor-jasmine-vitest
migration to automatically convert your Jasmine tests to Vitest tests,
which has been substantially improved to cover more cases and produce better code in v22.
It gained a flag --fake-async to convert fakeAsync tests to Vitest fake timers
utilities vi.useFakeTimers(), vi.advanceTimersByTimeAsync(n), etc.
That's great if you don't want to keep using Zone.js for these migrated tests.
We are currently migrating fairly large projects to Vitest,
and we adopted a dual setup, with ng test running the old Karma tests,
and a new vitest task running the migrated Vitest tests.
We can then migrate tests one by one,
using:
ng g refactor-jasmine-vitest --browser-mode --add-imports --fake-async --include src/app/user/user.ts
For components, we usually rewrite the test to leverage all the great features of Vitest Browser Mode (check out our article about Angular tests with Vitest Browser Mode to learn more about it). For other entities, the migration produces a more straightforward conversion, just replacing Jasmine functions by their Vitest equivalents. A few things are not automatically converted, and you'll sometimes see a TODO appearing in the migrated tests when that's the case.
If you still use Karma in your current project, you'll see that ng update
adds istanbul-lib-instrument as a dev dependency to your project.
This library, used for code coverage, was a direct dependency of the CLI before,
and it is now an optional peer dependency to avoid downloading it when you don't need it (for example if you are using Vitest instead of Karma).
Let's talk about the new features now: the unit-test builder gained two new options.
The first one is a quiet flag to suppress the build summary and stats table of the test build in the console,
which can be useful when running tests in watch mode and you only care about the running/failed tests.
This option is true by default locally and false in CI,
but you can override it in your angular.json configuration,
or via the --quiet flag when running the tests.
The second one is --isolate to run tests in isolated mode for Vitest only,
using native isolation (running tests in separate threads or processes).
Dev server
It is now possible to set the dev server port via process.env.PORT: PORT=4203 ng serve.
The environment variable takes precedence over the --port flag and the value in angular.json.
Build
The subresourceIntegrity option now generates an import map with the integrity hashes of the lazy-loading chunks,
whereas it used to only add the integrity hashes as attributes of the script tags in the index.html file.
As we are talking about lazy-loading chunks, an important change is that the chunk optimization introduced in Angular v18.1 is now enabled by default in production builds.
It used to be an opt-in optimization that allowed you to reduce the number of your lazy-loading chunks,
by using NG_BUILD_OPTIMIZE_CHUNKS=1 ng build.
The NG_BUILD_OPTIMIZE_CHUNKS is still available,
but it is now used to disable the optimization if needed (by passing 0),
or to customize it by specifying the threshold of lazy chunks required to trigger optimization,
with 3 as the default
(for example NG_BUILD_OPTIMIZE_CHUNKS=4 to only optimize if there are at least 4 lazy chunks).
The optimizer switched back to Rollup by default
(Angular v20.2 switched to the experimental Rolldown, written in Rust)
but Rolldown is still available as an experimental optimizer if you want to give it a try,
by using the NG_BUILD_CHUNKS_ROLLDOWN environment variable.
Summary
Angular v22 is a major step in the framework's current direction: more signal-friendly defaults, stable APIs for resources and Signal Forms.
The Angular team announced that they are working on a @boundary block in templates,
to let us catch errors in the template and display a fallback UI instead of breaking the whole page.
We may say see this lands in Angular v22.1 or v23, but it is definitely something to look forward to!
Stay tuned!
All our materials (ebook, online training and training) are up-to-date with these changes if you want to learn more!
← Article plus ancien
What's new in Angular 21.2?
Étiquettes
Nos livres en vente


Prochaines sessions de formation
- Du 15 au 18 juin 2026Angular : de Ninja à Héros(à distance)
- Du 15 au 18 juin 2026Angular : de Ninja à Héros(à distance)
- Du 22 au 25 juin 2026Vue : de Zéro à Ninja(à distance)
