🏗️ Framework
Templates & Compiler
a) Templates can access private component members — PR
Angular templates can now read and call TypeScript private members declared directly on their component.
import {
ChangeDetectionStrategy,
Component,
} from '@angular/core';
@Component({
selector: 'app-status',
template: `<p>{{ _status }}</p>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class StatusComponent {
private readonly _status = 'Ready';
}Previously, the template type checker reported an error for _status. Angular 22.2 allows this direct access, which is helpful for members that exist only to support the component template.
This applies to TypeScript private members of the component itself. It does not make a nested object’s private properties accessible, and it does not apply to JavaScript #private fields.
b) Catch misspelled output bindings — PR
The new strictUnclaimedEventNames option catches camelCase output names that do not belong to the component and are not native DOM events:
<!-- Typo: itemSelecetd instead of itemSelected ❌ -->
<product-list (itemSelecetd)="selectProduct($event)" />Enable it explicitly in tsconfig.json:
{
"angularCompilerOptions": {
"strictUnclaimedEventNames": true
}
}It is disabled by default and intentionally ignores lowercase or dash-separated custom events to reduce false positives.
Core & Reactivity
a) Catch errors from dynamically created views — PR
Angular can now catch rendering and lifecycle errors from a component created with ViewContainerRef.
private readonly _container = inject(ViewContainerRef);
showReport(): void {
this._container.createComponent(ReportComponent, {
onError: (error: Error): void => console.error('Report failed', error),
});
}Previously, these errors reached the global ErrorHandler. Now a dynamically created view can handle its own failure. The new error-boundary APIs are available as Developer Preview.
b) Read an element’s Injector from a query — PR
viewChild() and contentChild() can now return the injector attached to the matched element.
@Component({
imports: [ThirdPartyWidgetComponent],
template: `<third-party-widget #widget />`,
})
export class WidgetHostComponent {
readonly widgetInjector = viewChild('widget', { read: Injector });
}This lets wrapper components access providers exactly as the child element sees them. It removes the need to create a helper directive just to capture that injector. This is a public API.
Forms
a) Permanently hidden fields in Signal Forms — PR
Signal Forms can now mark a field as permanently hidden. Call hidden() with only the field path, without a condition or configuration object.
import { signal } from '@angular/core';
import { form, hidden } from '@angular/forms/signals';
const profile = signal({
name: '',
internalId: '',
});
const profileForm = form(profile, (path) => {
hidden(path.internalId);
});internalId stays hidden even when its value changes. Angular also excludes the field from active validation while it is hidden.
This is useful for fields that belong to the model but should never appear in the current form, such as internal identifiers or values managed by the application.
b) Signal Forms explain their behavior to AI agents — PR
An experimental WebMCP Signal Form can be filled and submitted by a browser AI agent. Angular now tells the agent two things about that form automatically:
{
readOnlyHint: false,
untrustedContentHint: false,
}readOnlyHint: false: submitting the form changes the page or application state.untrustedContentHint: false: Angular currently considers the form response trusted.
There is nothing new to configure. This is extra safety information that Angular adds behind the scenes. WebMCP support is still experimental.
Router
a) Throw RedirectCommand from guards and resolvers — PR
Guards and resolvers can now throw a RedirectCommand to stop the current navigation and redirect immediately.
import { inject } from '@angular/core';
import {
CanActivateFn,
RedirectCommand,
Router,
} from '@angular/router';
export const authGuard: CanActivateFn = () => {
const router = inject(Router);
throw new RedirectCommand(router.parseUrl('/login'));
};Previously, a nested helper had to return the redirect through every function in the call chain. Now it can throw the command directly, keeping the guard or resolver return type focused on its normal result.
b) Public containsTree API for URL matching — PR
The Router now exports containsTree, allowing applications and libraries to compare two UrlTree objects directly.
import { inject, Injectable } from '@angular/core';
import { containsTree, Router } from '@angular/router';
@Injectable({ providedIn: 'root' })
export class UrlMatcher {
private readonly _router = inject(Router);
isInside(url: string, parent: string): boolean {
return containsTree(
this._router.parseUrl(url),
this._router.parseUrl(parent),
);
}
}Calling isInside('/products/42/details', '/products') returns true.
By default, paths and query parameters use subset matching. You can pass options such as { paths: 'exact' } when you need stricter behavior.
This is useful for custom navigation components, breadcrumbs and route-aware libraries that need URL matching without depending on private Router internals.
c) ⚠️ Automatic route-injector cleanup is now stable — PR
Detached routes can leave their injectors—and the services created inside them—alive. Enabling this feature lets Angular destroy an injector when its route is no longer active or saved for reuse.
The feature is now stable. Only its name changes:
// Before ❌
provideRouter(routes, withExperimentalAutoCleanupInjectors());
// Angular 22.2 ✅
provideRouter(routes, withAutoCleanupInjectors());The old function still works, but it is deprecated. Custom RouteReuseStrategy implementations also get stable hooks for telling Angular which saved routes must keep their injectors.
d) Load route data with Signals and Resources — PR
Router Resources are a reactive alternative to resolvers. They can load data for a route, expose loading and error states, and reload without navigating again.
First, enable them:
provideRouter(routes, withRouterResources());Then define the resource on the route:
{
path: 'products/:id',
component: ProductPageComponent,
resources: (route) => {
const products = inject(ProductService);
return {
product: resource({
params: () => String(route.params()['id']),
loader: ({ params: id }) => products.get(id),
}),
};
},
}Resources from parent and child routes load in parallel, avoiding the sequential waits that nested resolvers can create. They are public in Angular 22.2, but remain in Developer Preview.
Developer Tooling
a) Explicit workspace roots for Angular MCP — PR
The Angular MCP command now accepts --root. It defines which directories the MCP server may access and use for workspace discovery.
ng mcp --root ./frontend --root ./sharedYou can repeat the option for multi-root workspaces. This also lets an MCP host start Angular outside the project root while still passing the allowed directories explicitly.
b) Test directives without creating a host component — PR
Previously, testing a directive usually required creating a temporary host component. The new public TestBed.createDirective() API creates the directive and its HTML element directly.
import { Directive } from '@angular/core';
import { TestBed } from '@angular/core/testing';
@Directive({
selector: '[appHighlight]',
host: { class: 'highlighted' },
})
class HighlightDirective {}
it('highlights its host element', (): void => {
const fixture = TestBed.createDirective(HighlightDirective, {
tagName: 'button',
});
fixture.detectChanges();
expect(fixture.nativeElement.classList.contains('highlighted')).toBe(true);
});The fixture gives us the directive instance, its real host element and change detection. Tests become shorter because the artificial host component disappears.
c) WebMCP tools can declare safety annotations — PR
WebMCP lets a web application expose actions to browser AI agents. A tool can now describe its safety characteristics through two annotations:
import { provideExperimentalWebMcpTools } from '@angular/core';
export const webMcpProviders = provideExperimentalWebMcpTools([
{
name: 'readAccountStatus',
description: 'Reads the current account status.',
inputSchema: { type: 'object', properties: {} },
annotations: {
readOnlyHint: true,
untrustedContentHint: false,
},
execute: () => ({
content: [{ type: 'text', text: 'The account is active.' }],
}),
},
]);Here, readOnlyHint: true tells the agent that calling the tool changes nothing. untrustedContentHint: false says its response is controlled by the application rather than an external source.
The names are a bit technical, but the idea is simple: the AI agent gets more context before deciding whether and how to use a tool. These are hints, not security enforcement, and WebMCP remains experimental.
Language Tooling
a) Editor support for @boundary blocks — PR
The Angular Language Service now understands the new error-boundary syntax:
@boundary {
<payment-widget />
} @error (let error) {
<p>Payment failed: {{ error.message }}</p>
}Editors can now highlight, fold and navigate these blocks correctly, as well as provide hover information. This does not add another application API; it adds editor support for the Developer Preview error-boundary feature.
🛠️ CLI & Build Tooling
Build System
a) Angular Linker moves from Babel to OXC — PR
The Angular Linker in the build pipeline now uses oxc-parser and magic-string instead of @babel/core and the Angular Linker Babel plugin.
OXC provides the syntax tree and exact token positions, while magic-string updates partial Angular declarations directly in the original source. This removes Babel from this part of the build pipeline and reduces the amount of work needed to link partially compiled libraries.
b) ⚠️ Separate browser and server build statistics — PR
Angular can now produce separate statistics for browser, server and initial bundles. This makes the output easier to load into bundle analyzers, especially for SSR applications where one combined file contains too much unrelated data.
Generate the files with the existing option:
ng build --stats-jsonThe generated output has changed:
// Before ❌
dist/stats.json
// After ✅
dist/browser-stats.json
dist/browser-initial-stats.json
dist/server-stats.json # SSR only
dist/server-initial-stats.json # SSR onlyThe initial files contain only the initial page bundles, while the full files include every corresponding browser or server output.
This is a breaking change for scripts and CI jobs that expect stats.json. Update those paths to the appropriate new file. For example, use browser-initial-stats.json when you only want to inspect the initial browser payload.
⚠️ Breaking Changes & Deprecations
Angular 22.2 does not declare any formal breaking changes. However, it includes one build-output change that can break existing pipelines and two deprecations.
Build statistics file names — PR #33870
EnablingstatsJsonno longer createsstats.json. Browser builds now generatebrowser-stats.jsonandbrowser-initial-stats.json; SSR builds also generate their server equivalents. Update scripts and analyzers that expect the old filename.Deprecated experimental Router cleanup name — PR #70443
ReplacewithExperimentalAutoCleanupInjectors()with the stablewithAutoCleanupInjectors(). The deprecated function still delegates to the stable implementation, so this migration does not need to happen all at once.Deprecated
splittingoption in the unit-test builder — PR #34031
Removesplittingfrom the options of@angular/build:unit-test. It is no longer necessary with Vitest 5 and has no replacement.
Thanks for reading so far 🙏
I’d like to have your feedback, so please leave a comment, clap or follow. 👏
Spread the Angular love! 💜
If you liked it, share it among your community, tech bros and whoever you want! 🚀👥
Don’t forget to follow me and stay in the loop: 📱
Thanks for being part of this Angular adventure! 👋😁


