Introduction
L'année dernière, Angular 21 posait les jalons d'un futur sans Zone.js et introduisait les prémices des formulaires réactifs basés sur les Signaux. Angular 22 transforme l'essai. Avec le passage de OnPush par défaut, la stabilisation des Signal Forms et l'intégration des outils de débogage assistés par l'IA, cette version marque le point de non-retour vers une architecture "Signals-First".
Table des Matières
🏗️ Core & Change Detection
Le gros morceau de cette version, c'est le changement de philosophie sur la détection de changement. On quitte enfin l'ère du "on vérifie tout, tout le temps" !
OnPush par défaut et stratégie Eager
Jusqu'ici, sans précision de ta part, un composant utilisait la stratégie Default (ou CheckAlways). À chaque micro-événement dans l'application, Angular parcourait tout l'arbre. C'était "magique", mais coûteux.
Désormais, tous les nouveaux composants utilisent ChangeDetectionStrategy.OnPush par défaut. L'ancienne stratégie Default est renommée Eager (avide) pour être plus explicite : elle indique qu'un composant demande à être vérifié de manière proactive, même sans preuve de changement.
Le compilateur Angular a été mis à jour. Si tu ne spécifies rien, c'est OnPush qui s'applique. Pour tes anciens composants, le ng update injectera explicitement la stratégie Eager pour garantir que rien ne casse.
// Avant migration
@Component({ selector: 'app-old', template: '...' })
// Après migration vers Angular 22
@Component({
selector: 'app-old',
template: '...',
changeDetection: ChangeDetectionStrategy.Eager // Injecté automatiquement
})Ta mission post-migration : Ne plus jamais écrire Eager pour un nouveau composant. Considère chaque occurrence de Eager dans ton code comme une dette technique à transformer progressivement en composants OnPush basés sur les Signaux.
ApplicationRef.bootstrap() s'aligne sur createComponent()
Angular propose depuis longtemps deux façons de monter dynamiquement un composant. Le problème ? Ces deux API ne parlaient pas le même langage.
createComponent() acceptait un objet de configuration riche : un élément hôte, des directives, des bindings. ApplicationRef.bootstrap(), elle, se contentait d'un simple sélecteur CSS. Impossible de lui passer des directives ou des bindings sans contournement.
// Avant : limité à un sélecteur CSS
appRef.bootstrap(AppComponent, '#app');
// Après : un objet de configuration complet
appRef.bootstrap(AppComponent, {
hostElement: document.getElementById('app'),
directives: [{ type: ThemeDirective, bindings: [inputBinding('theme', () => 'dark')] }],
bindings: [inputBinding('config', () => ({ env: 'production' }))],
});Là où ça devient vraiment intéressant, c'est quand on monte plusieurs composants racine sur la même page (comme sur une application legacy avec plusieurs "îlots" Angular). Chaque composant peut désormais recevoir sa propre configuration via bindings et directives, là où avant on était obligé de tout déverser dans les providers globaux de l'application :
// Widget panier : son propre contexte
appRef.bootstrap(CartWidget, {
hostElement: document.getElementById('cart'),
bindings: [inputBinding('apiUrl', () => 'https://api-cart.example.com')],
});
// Widget recherche : totalement isolé, URL différente
appRef.bootstrap(SearchWidget, {
hostElement: document.getElementById('search'),
bindings: [inputBinding('apiUrl', () => 'https://api-search.example.com')],
});L'ancienne syntaxe reste valide, aucune migration nécessaire. Seul changement notable : le second paramètre perd son ancien type any pour Element. Fini les erreurs silencieuses !
Directive Composition API : Finies les collisions
Introduite dans Angular 15, la Directive Composition API (hostDirectives) permet de composer des comportements en attachant des directives à d'autres directives ou composants. Mais elle butait sur une limite frustrante : si deux composants partageaient la même directive hôte et qu'un troisième tentait de les combiner, Angular plantait avec l'erreur NG0309: Directive AppearanceDirective matches multiple times on the same element. Directives can only match an element once.
// Une directive de style partagée
@Directive({ selector: '[appAppearance]' })
export class AppearanceDirective {}
// Un bouton qui l'utilise
@Component({
selector: 'app-button',
hostDirectives: [AppearanceDirective],
})
export class ButtonComponent {}
// Un item de dropdown qui l'utilise aussi
@Component({
selector: 'app-dropdown-item',
hostDirectives: [AppearanceDirective],
})
export class DropdownItemComponent {}
// ❌ Avant Angular 22 : NG0309 — AppearanceDirective matchée deux fois
@Component({
selector: 'app-sidebar-item',
hostDirectives: [ButtonComponent, DropdownItemComponent],
})
export class SidebarItemComponent {}
Heureusement, Angular 22 résout automatiquement ces collisions selon deux règles claires :
Règle 1 : Le template gagne toujours. Si une directive est présente à la fois dans le template et dans les hostDirectives, c'est l'instance du template qui l'emporte.
Règle 2 : Les host directives en double fusionnent. Si la même directive arrive plusieurs fois uniquement via hostDirectives, Angular fusionne intelligemment leurs inputs/outputs en une seule instance.
// ✅ Angular 22 : fonctionne, AppearanceDirective est dédupliquée automatiquement
@Component({
selector: 'app-sidebar-item',
hostDirectives: [ButtonComponent, DropdownItemComponent],
})
export class SidebarItemComponent {}
⚠️ Le cas qui reste bloquant : Les alias contradictoires
Il reste un seul cas de figure où le compilateur lèvera une erreur : si les deux sources exposent la même directive avec des alias contradictoires sur les mêmes propriétés (inputs ou outputs).
// ButtonComponent expose 'variant' sous l'alias 'buttonVariant'
@Component({
selector: 'app-button',
hostDirectives: [{
directive: AppearanceDirective,
inputs: ['variant: buttonVariant'],
}],
})
export class ButtonComponent {}
// DropdownItemComponent expose le même input sous 'dropdownVariant'
@Component({
selector: 'app-dropdown-item',
hostDirectives: [{
directive: AppearanceDirective,
inputs: ['variant: dropdownVariant'],
}],
})
export class DropdownItemComponent {}
// ❌ Erreur à la compilation ou au runtime :
// Input "variant" from AppearanceDirective is exposed under the following
// conflicting names: "buttonVariant" and "dropdownVariant".
// An input can only be exposed under a single name.
@Component({
selector: 'app-sidebar-item',
hostDirectives: [
{ directive: ButtonComponent },
{ directive: DropdownItemComponent },
],
})
export class SidebarItemComponent {}
// ✅ Fix : aligner les alias des deux côtés
// inputs: ['variant: appearanceVariant'] dans ButtonComponent ET DropdownItemComponentAngular ne peut pas choisir lequel des deux alias garder sans risquer de casser l'un des composants. Le message d'erreur est explicite : il indique précisément quel input pose problème et sous quels noms contradictoires il est exposé. La correction est simple, aligner les alias au sein de tes deux composants.
injectAsync : lazy-loader ses services Angular sans quitter le DI
Cette version introduit injectAsync, un petit helper ultra-pratique qui vient résoudre un problème vieux comme le monde (ou du moins aussi vieux que le code-splitting dans Angular) : comment charger un service lourd seulement quand on en a besoin, tout en gardant les bénéfices du système d'injection de dépendances ?
Prenons un exemple concret : une application de gestion de commandes. Sur la page de détail d'une commande, un bouton "Exporter en PDF" permet à l'utilisateur de télécharger sa facture. Pour ça, on a un InvoiceExportService qui encapsule une bibliothèque de génération PDF qui est lourde par nature.
Le problème ? Dès qu'on injecte ce service dans le composant, Angular embarque toutes ses dépendances dans le bundle initial. Chaque utilisateur paie le coût de la bibliothèque PDF, même ceux qui ne cliquent jamais sur "Exporter".
@Component({ ... })
export class OrderDetailComponent {
// ❌ La lib PDF chargée pour tout le monde, même les utilisateurs en lecture seule
private readonly invoiceExport = inject(InvoiceExportService);
}Avant Angular 22, contourner ce problème nécessitait d'utiliser Injector à la main :
@Component({ ... })
export class OrderDetailComponent {
private readonly injector = inject(Injector);
async onExportClick() {
const svc = await import('../invoice-export.service').then(m =>
this.injector.get(m.InvoiceExportService)
);
svc.download(this.order());
}
}Ça fonctionne, mais c'est du boilerplate répétitif : injecter Injector, gérer la Promise, reproduire le pattern dans chaque composant qui en a besoin. Angular 22 encapsule tout ça dans un helper dédié :
import { injectAsync } from '@angular/core';
@Component({ ... })
export class OrderDetailComponent {
private readonly invoiceExport = injectAsync(
() => import('../invoice-export.service').then(m => m.InvoiceExportService)
);
async onExportClick() {
const svc = await this.invoiceExport();
svc.download(this.order());
}
}Sous le capot, Angular capture automatiquement le contexte d'injection courant, résout le service via le DI avec toutes ses sous-dépendances, et met le résultat en cache, le module n'est importé qu'une seule fois, les appels suivants à this.invoiceExport() seront instantanément.
Aller plus loin avec le prefetch
Pour parfaire l'expérience de tes utilisateurs, la signature d' injectAsync accepte un objet d'options permettant de configurer une stratégie de préchargement :
function injectAsync<T>(
loader: () => Promise<ProviderToken<T>>,
options?: { prefetch?: PrefetchTrigger }
): () => Promise<T>loader: une fonction qui retourne unPromise<ProviderToken<T>>. En pratique : un dynamic import suivi d'un.then(m => m.MonService).options.prefetch: une stratégie de pré-chargement optionnelle.
L'option prefetch permet de déclencher le chargement en arrière-plan avant que l'utilisateur n'interagisse, sans impacter le bundle initial.
readonly heavySvc = injectAsync(() =>
import('./heavy.service').then((m) => m.HeavyService),
{ prefetch: () => onIdle({ timeout: 100 }) }
);Grâce à onIdle, Angular profite des moments creux du navigateur pour pré-charger discrètement le module. Résultat : ton bundle initial reste léger, et quand l'utilisateur clique sur "Exporter", le service est déjà prêt !
Resource et gestion des statuts
Statuts spéciaux dans les params de resource
Jusqu'à présent, le contrôle de l'état d'une resource depuis sa fonction params était limité : soit tu retournais une valeur pour déclencher le chargement, soit undefined pour la mettre en mode idle. Il n'y avait aucun moyen propre pour forcer un état loading ou error directement depuis les paramètres. Angular 22 comble ce manque avec ResourceParamsStatus et de la fonction chain().
Le problème
Imagine une resource dont les paramètres dépendent d'une autre resource. Si la resource parente est encore en cours de chargement, que fait-on ? Tu étais condamnés à gérer ce manuellement avec des conditions verbeuses dans params :
// ❌ Avant Angular 22 : Gestion manuelle et verbeuse
const userResource = resource({ loader: () => fetchUser() });
const postsResource = resource({
params: () => {
const user = this.userResource.value();
if (!user) return undefined; // Impossible de distinguer idle de loading
return { userId: user.id };
},
loader: ({ params }) => fetchPosts(params.userId),
});La solution : ResourceParamsStatus et chain()
Désormais, la fonction params reçoit désormais un contexte (ResourceParamsContext) contenant la méthode chain. Cette méthode magique propage automatiquement les états de la ressource parente :
import { resource, ResourceParamsStatus } from '@angular/core';
const postsResource = resource({
params: ({ chain }) => ({
// chain() propage automatiquement idle, loading et error de userResource
userId: chain(this.userResource).id,
}),
loader: ({ params }) => fetchPosts(params.userId),
});chain(resource) retourne la valeur résolue de la resource et throw automatiquement le bon ResourceParamsStatus si la resource parente est en idle, loading ou error. Plus besoin de conditions if à rallonge !
Tu peux également ces statuts de manière explicite, ou utiliser une Error classique, pour basculer instantanément ta ressource dans l'état souhaité :
| Valeur throwée | État resultant |
|---|---|
ResourceParamsStatus.IDLE |
idle |
ResourceParamsStatus.LOADING |
loading |
Error |
error |
const filteredResource = resource({
params: () => {
if (!hasPermission()) throw new Error('Accès refusé');
const query = searchQuery();
if (!query) throw ResourceParamsStatus.IDLE; // Resource inactive
if (isLocked()) throw ResourceParamsStatus.LOADING; // Forcer l'état loading
return { q: query };
},
loader: ({ params }) => search(params),
});Valeurs synchrones dans les stream resources
Cette amélioration concerne un cas précis : les ressources de type stream dans un contexte de rendu côté serveur (SSR).
Avant ce changement, le loader d'une stream resource devait obligatoirement retourner une Promise. Même si la donnée était immédiatement disponible, la resource passait inévitablement par un état loading avant de se résoudre.
Ce détail technique a une conséquence directe sur l'hydratation :
- En SSR, Angular pré-génère le DOM côté serveur et le transfère au client
- Au moment de l'hydratation sur le client, si une ressource bascule en état
loading, Angular considère que le rendu graphique n'est plus valide. - Résultat : Angular détruit instantanément le DOM hydraté pour le reconstruire de zéro. Tu perds ainsi le bénéfice du SSR, les écouteurs d'événements et les interactions utilisateur en cours.
dataResource = resource({
stream: async () => {
// ❌ Même avec une donnée déjà disponible, on passe par async
// → la resource reste en "loading" le temps de la micro-task
// → Angular détruit le DOM hydraté
return Promise.resolve<AppState>({ mode: 'show', label: 'Bonjour' })
.then(data => signal({ value: data }));
},
});Angular 22 élargit la signature du loader pour lui permettre d'accepter un Signal directement, sans passer par une Promise :
readonly state = signal<AppState>({ mode: 'show', label: 'Coucou du cache' });
dataResource = resource({
stream: () => {
const cached = this.state();
if (cached) {
// ✅ Résolution synchrone : la resource est immédiatement en état "resolved"
// → le DOM hydraté est préservé
return signal({ value: cached });
}
// Sinon, retour classique via Promise
return Promise.resolve<AppState>({ mode: 'show', label: 'Bonjour' })
.then(data => signal({ value: data }));
},
});En retournant un Signal directement (et non une Promise<Signal>), la resource se résout dès le premier tick de l'effect, sans passer par l'état loading. Le DOM rendu côté serveur reste intact.
Signature mise à jour
type ResourceStreamingLoader<T, R> = (param: ResourceLoaderParams<R>) =>
Signal<ResourceStreamItem<T>> // Nouveau : résolution synchrone
| PromiseLike<Signal<ResourceStreamItem<T>>> // Existant : résolution async
| undefined; // Nouveau : resource inactive En pratique, ce changement est surtout pertinent pour les stratégies de cache SSR avec TransferState, où la donnée est disponible au moment de l'initialisation. Pour les cas purement client (sans SSR), le comportement existant reste inchangé.
Le cache automatique en SSR avec transferCacheKey
Lorsque tu développes une application Angular en Server-Side Rendering (SSR), le serveur exécute ton code, appelle l'API pour récupérer les données, génère le HTML et l'envoie au navigateur.
Le problème lors de l'hydratation, c'est que le client (le navigateur) démarre à son tour et réexécute la ressource. Elle relançait donc exactement la même requête HTTP vers ton API.
Problème réglé avec l'introduction de l'option transferCacheKey directement au cœur de la configuration de resource et rxResource.
readonly productId = signal(42);
// String simple
readonly productDetailResource = resource({
params: () => ({ id: this.productId() }),
loader: async ({ params }) => (await fetch(`/api/products/${params.id}`)).json(),
id: 'product-detail-cache-key'
});
// Ou avec une StateKey typée (recommandé pour les TypeScript strict)
readonly PRODUCT_DETAIL_KEY = makeStateKey<ProductDetial>('product-detail-cache-key');
readonly productDetailResourceWithStateKey = resource({
params: () => ({ id: this.productId() }),
loader: async ({ params }) => (await fetch(`/api/products/${params.id}`)).json(),
id: this.PRODUCT_DETAIL_KEY
});🤖 Intégration IA
Angular 22 ne se contente pas d'optimiser le code existant, il ouvre grand la porte aux outils de développement du futur en intégrant nativement des fonctionnalités pensées pour l'Intelligence Artificielle.
Outils de débogage IA : signal_graph & di_graph
Diagnostiquer une application Angular en mode développement demande souvent de fouiller les DevTools, d'ajouter des console.log et de reconstruire mentalement des graphes de dépendances qui peuvent être très profonds. Deux outils de débogage pensés pour les agents IA sont désormais exploitables par des outils comme Claude via Chrome DevTools MCP.
Ces deux outils sont enregistrés automatiquement au démarrage de la plateforme en mode développement, et dés-enregistrés à sa destruction, sans aucune configuration nécessaire de ta part.
angular:signal_graph cartographier la réactivité d'un composant
Cet outil expose le graphe des signaux d'un composant donné à partir d'un élément du DOM. Il remonte les effets enregistrés sur l'injecteur du composant, puis parcourt récursivement leurs dépendances pour identifier tous les signaux utilisés dans le template ou dans un effect().
En pratique : face à un composant qui ne se met pas à jour comme attendu, un agent IA peut appeler angular:signal_graph avec le sélecteur de l'élément concerné et obtenir instantanément le graphe complet des dépendances réactives, quels signaux sont lus, depuis quels effets, et comment ils sont connectés entre eux.

angular:di-graph explorer la hiérarchie d'injection
Le second outil expose l'intégralité du graphe d'injection de l'application. Il localise les structures internes (LView), remonte les hiérarchies d'injecteurs et collecte en parallèle les injecteurs d'environment.
Le résultat : pour un composant donné, un agent IA peut répondre à des questions comme "quel provider résout ce token ?", "à quel niveau de l'arbre ce service est-il fourni ?", ou encore "deux composants partagent-ils bien la même instance ?".
Ces deux outils partagent une même philosophie : ils ne sont pas destinés à être appelés manuellement depuis le code applicatif, mais à être consommés par des agents IA qui peuvent interpréter ces graphes et en tirer des diagnostics. Ils sont désactivés en mode production.
Exposer son application Angular aux agents IA avec WebMCP
Le Model Context Protocol (MCP) est un standard émergent qui définit comment un agent IA peut invoquer des outils exposés par une application. Angular 22 intègre WebMCP nativement dans son système d'injection via deux nouvelles API expérimentales : declareExperimentalWebMcpTool et provideExperimentalWebMcpTools.
Ces APIs portent le préfixe Experimental et évolueront avec la spécification WebMCP elle-même.declareExperimentalWebMcpTool
Elle enregistre un outil WebMCP lié à cycle de vie de l'injecteur courant. Si l'injecteur est détruit, l'outil se désenregistre automatiquement.
Voici comment transformer un simple ScoreService pour qu'un agent IA puisse interagir directement avec lui :
import { Injectable, signal, computed, declareExperimentalWebMcpTool, inject } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ScoreService {
private readonly _score = signal(0);
private readonly _history = signal<number[]>([0]);
readonly score = this._score.asReadonly();
readonly history = this._history.asReadonly();
readonly best = computed(() => Math.max(0, ...this._history()));
constructor() {
declareExperimentalWebMcpTool({
name: 'increment_score',
description: 'Incrémente le score du joueur.',
inputSchema: {
type: 'object',
properties: {
amount: { type: 'number', description: 'Valeur à ajouter' },
},
required: ['amount'],
additionalProperties: false,
},
execute: async ({ amount }) => {
const svc = inject(ScoreService);
svc.increment(amount as number);
return { content: [{ type: 'text', text: `Score : ${svc.score()}` }] };
},
});
declareExperimentalWebMcpTool({
name: 'reset_score',
description: 'Remet le score à zéro.',
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
execute: async () => {
const svc = inject(ScoreService);
svc.reset();
return { content: [{ type: 'text', text: `Score remis à zéro. Meilleur : ${svc.best()}` }] };
},
});
}
increment(by: number): void {
this._score.update(s => s + by);
this._history.update(h => [...h, this._score()]);
}
reset(): void {
this._score.set(0);
this._history.update(h => [...h, 0]);
}
}Deux points importants dans l'execute : il tourne dans le contexte d'injection de l'enregistrement, donc inject() y est disponible, pas besoin de passer les services en paramètre. Et puisque ScoreService est providedIn: 'root', les outils vivent aussi longtemps que l'application.
provideExperimentalWebMcpTools
C'est la version déclarative, un EnvironmentProviders qu'on pose dans le tableau providers de l'application ou d'une route. Idéal pour fournir des outils transversaux qui n'appartiennent à aucun service en particulier.
// app.config.ts
import { provideExperimentalWebMcpTools } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideExperimentalWebMcpTools([
{
name: 'get_app_version',
description: "Retourne la version de l'application.",
inputSchema: { type: 'object', properties: {} },
execute: async () => ({
content: [{ type: 'text', text: 'v22.0.0' }],
}),
},
]),
],
};Un plugin Chrome de test MCP liste les outils disponibles : get_app_version, increment_score, reset_score. L'exécution successive des trois outils montre get_app_version retourner "v22.0.0", increment_score met à jour le score en direct dans l'interface, et reset_score le remet à zéro et affiche le meilleur score.
Des outils liés aux fonctionnalités de tes routes
L'intérêt majeur de provideExperimentalWebMcpTools : apparaît lorsqu'il est placé dans les providers d'une route spécifique et combiné avec withExperimentalAutoCleanupInjectors(). L'outil s'enregistre à l'activation de la route et disparaît dès que l'utilisateur change de page. L'agent IA ne voit que les outils pertinents pour la page courante.
// app.routes.ts
export const routes: Routes = [
{
path: 'catalog',
loadComponent: () => import('./catalog/catalog.component'),
providers: [
provideExperimentalWebMcpTools([
{
name: 'search_products',
description: 'Recherche des produits par mot-clé dans le catalogue.',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Terme de recherche' },
},
required: ['query'],
},
execute: async ({ query }) => {
const svc = inject(CatalogService);
const results = svc.search(query as string);
return { content: [{ type: 'text', text: JSON.stringify(results) }] };
},
},
]),
],
},
{
path: 'cart',
loadComponent: () => import('./cart/cart.component'),
providers: [
provideExperimentalWebMcpTools([
{
name: 'checkout',
description: 'Déclenche le paiement du panier courant.',
inputSchema: { type: 'object', properties: {} },
execute: async () => {
const svc = inject(CartService);
await svc.checkout();
return { content: [{ type: 'text', text: 'Commande validée.' }] };
},
},
]),
],
},
];
// app.config.ts — activer le nettoyage automatique des injectors de routes
provideRouter(routes, withExperimentalAutoCleanupInjectors())- Sur
/catalog, ton agent IA verra et pourra utiliser l'outilsearch_products - Sur
/cart, il verra uniquement l'outilcheckout.
Les outils entrent et sortent du registre de l'IA de manière totalement transparente, sans aucune ligne de nettoyage manuel de ta part !
🎨 Template & Defer : La finesse du contrôle
Introduit dans les versions précédentes pour révolutionner le Lazy Loading au niveau du composant, le bloc @defer gagne en maturité en s'armant de mécanismes de contrôle beaucoup plus fins pour orchestrer le rendu.
Le timeout pour le trigger on idle
Jusqu'à présent, @defer (on idle) attendait sagement que le navigateur soit "au repos" (grâce à requestIdleCallback) pour charger le bloc concerné. Mais que se passe-t-il si ton application sollicite le navigateur en continu ? Le chargement du bloc pouvait être retardé indéfiniment. Désormais, tu peux passer un timeout optionnel :
@defer (on idle(2000)) {
<p class="loaded">Contenu chargé après idle (max 2000 ms)</p>
} @placeholder {
<p class="placeholder">En attente d'idle…</p>
}Personnalisation via IdleService
Si le comportement natif basé sur requestIdleCallback convient à la majorité des projets, tu peux avoir besoin d'un contrôle total sur ces timings : environnements de tests unitaires, gestion fine du rendu côté serveur (SSR), ou stratégies de planification sur mesure.
Angular 22 t'offre cette flexibilité en te permettant de surcharger entièrement ce moteur interne grâce au provider provideIdleServiceWith(). L'interface IdleService prend désormais en charge le type IdleRequestOptions, ce qui te permet de récupérer et d'honorer le paramètre timeout défini directement dans ton template HTML.
Voici comment mettre en place ton propre planificateur d’idle en utilisant un fallback basé sur setTimeout :
import { Injectable } from '@angular/core';
import { IdleService, IdleRequestOptions } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CustomIdleService implements IdleService {
private readonly callbacks = new Map<number, ReturnType<typeof setTimeout>>();
private nextId = 1;
requestOnIdle(callback: (deadline?: IdleDeadline) => void, options?: IdleRequestOptions): number {
const id = this.nextId++;
// On utilise un setTimeout, tout en respectant le timeout hérité du template
const delay = options?.timeout ?? 50;
const handle = setTimeout(() => callback(), delay);
this.callbacks.set(id, handle);
return id;
}
cancelOnIdle(id: number): void {
const handle = this.callbacks.get(id);
if (handle !== undefined) {
clearTimeout(handle);
this.callbacks.delete(id);
}
}
}Il ne te reste plus qu'à brancher ton service personnalisé dans la configuration globale de ton application :
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideIdleServiceWith } from '@angular/core';
import { CustomIdleService } from './custom-idle.service';
export const appConfig: ApplicationConfig = {
providers: [
provideIdleServiceWith(CustomIdleService)
]
};Des commentaires dans les balises HTML ?
Oui, tu as bien lu ! Le compilateur supporte maintenant les commentaires de type Javascript (// ou /* */) à l'intérieur même de la balise ouvrante d'un élément.
C'est un confort visuel très appréciable pour documenter des listes d'attributs complexes ou ajouter des rappels techniques directement là où ils font sens :
<button
// Ce bouton est désactivé pendant la phase de maintenance
/* FIXME : remplacer par une directive [appPermission] avant la v2.0 */
[disabled]="isMaintenanceMode"
/* Doit correspondre au tracking analytics défini dans tracking.service.ts */
data-track-id="submit-main-cta"
(click)="onSubmit()"
>
Valider
</button>Cette nouveauté aligne Angular sur ce que proposent déjà d'autres frameworks modernes comme Svelte. À terme, cela ouvre surtout la porte au support de directives de compilation comme // @ts-ignore directement dans tes templates. Un détail, mais qui sera très vite indispensable dans des codebase complexes.
Vérification exhaustive avec @default never()
Lorsque tu utilises le bloc @switch pour gérer l'affichage selon différents scénarios, comment être sûr à 100 % que tu n'as pas oublié un cas en chemin ? Jusqu'ici, le compilateur d'Angular avait du mal à valider cette exhaustivité dès que la variable était imbriquée dans une union de types complexes.
Angular 22 corrige le tir et nous apporte une sécurité bien plus stricte grâce à la syntaxe never(expression) au sein du bloc @default. Elle permet d'indiquer explicitement à TypeScript quelle variable globale il doit surveiller pour vérifier que tous les cas ont bien été traités.
type HideState = { mode: 'hide' };
type ShowState = { mode: 'show'; label: string };
type AppState = HideState | ShowState;
// Côté composant
readonly state = signal<AppState>({ mode: 'show', label: 'Bonjour' }); @switch (state().mode) {
@case ('show') {
<p class="state-show">Mode show — label : {{ $any(state()).label }}</p>
}
@case ('hide') {
<p class="state-hide">Mode hide</p>
}
@default {
<!-- Si un nouveau mode est ajouté sans mettre à jour ce switch,
le compilateur produira une erreur de type -->
never(state())
}
}Plus besoin de croiser les doigts lors des grosses mises à jour. Si demain tu modifies ton code pour ajouter un troisième état (par exemple un mode 'loading') mais que tu oublies de mettre à jour ce template, le projet refusera de compiler. C'est l'assurance de ne plus jamais laisser un bug s'échapper par simple oubli !
Language Service : L'IDE qui comprend Angular
Cette version apporte une salve d'améliorations majeures au Language Service qui vont transformer ton quotidien dans VS Code (et tous les éditeurs compatibles avec le protocole LSP). L'outillage d'Angular gagne en précision pour t'éviter des erreurs avant même la phase de compilation.
Deux nouveautés majeures font leur apparition :
- Les Inlay Hints : Ton IDE affiche désormais de petits indices contextuels directement dans le template pour t'indiquer les types ou les noms des paramètres attendus.
- Le support des Document Symbols : Il devient beaucoup plus facile de naviguer dans la structure interne de tes templates HTML complexes grâce à une cartographie complète de tes éléments lue par l'IDE.
La navigation safe affine enfin le type-narrowing
Angular 22 corrige un comportement historique du compilateur de templates concernant l'opérateur de navigation sécurisée ?.. Jusqu'ici, l'utiliser dans une condition @if n'avait aucun impact sur la déduction des types (le type narrowing) dans le reste du bloc. Désormais, le compilateur s'aligne sur le comportement standard de TypeScript : dès qu'un ?. valide la présence d'une donnée, le type est automatiquement affiné et débarrassé de ses valeurs nulles ou indéfinies pour tout le reste du bloc conditionnel.
Attention toutefois si tu manipules des signaux, écrire une condition directement sur le signal, comme @if (user()?.name), ne suffira pas à rassurer le compilateur. Pour TypeScript, un appel de fonction, symbolisé par les parenthèses () , n'est pas une référence stable : rien ne garantit qu'il renverra la même valeur deux lignes plus bas.
Pour en profiter, il faut donc associer cette nouveauté à la syntaxe @let (disponible depuis Angular 18) afin de figer le résultat du Signal dans une variable locale. Une fois la référence stabilisée, la magie d'Angular 22 opère :
@let user = userSignal();
@if (user?.name) {
<!-- Ici, user est garanti non-null par le compilateur -->
<p class="user-name">Utilisateur : {{ user.name }}</p>
@if (user.address) {
<!-- user.address est garanti non-null ici -->
<p class="user-city">Ville : {{ user.address.city }}</p>
}
} @else {
<p class="no-user">Aucun utilisateur</p>
}Puisque le compilateur sait désormais que te variables ne sont plus nulles à l'intérieur de ces blocs, tous tes anciens ?. ou ?? restés dans le code deviennent obsolètes. Si tu utilises la vérification stricte (strictTemplates), cela va lever des erreurs de diagnostic au build. C'est l'occasion idéale pour nettoyer tes templates !
L'optional chaining retourne enfin undefined
Encore un alignement très sain avec les standards du web ! Jusqu'ici, lorsqu'une chaîne d'optional chaining était interrompue dans un template (par exemple product()?.details?.price si la propriété details n'existait pas), Angular retournait historiquement la valeur null. Ce comportement était un héritage du passé, mais il entrait en contradiction directe avec la spécification JavaScript, où l'opérateur ?. renvoie nativement undefined.
Si tu as des conditions strictes basées sur null ou des opérateurs de coalescence (??) calibrés spécifiquement sur cette ancienne valeur, ce changement peut impacter tes logiques d'affichage.
Pour t'accompagner dans cette transition sans bloquer ton application, Angular introduit une fonction utilitaire de secours : $safeNavigationMigration(). Elle permet d'envelopper ton expression pour forcer temporairement le retour à null, le temps de mettre à jour tes composants ou tes vérifications :
<p>
Prix :
<code class="price-new">
{{ product()?.details?.price === undefined ? 'Non défini' : product()?.details?.price }}
</code>
</p>
<p>
Prix (migration) :
<code class="price-migration">
{{ $safeNavigationMigration(product()?.details?.price) === null ? 'null (ancien)' : product()?.details?.price }}
</code>
</p>Cette fonction de compatibilité est idéale pour lisser ta migration. Elle te permet de passer à Angular 22 sereinement, puis de nettoyer tes templates petit à petit en adaptant tes vérifications aux standards modernes de JavaScript.
🛠️ Outillage & DX : Le confort de développement avant tout
Documentation d'Angular directement intégrée au profiler Chrome
Petite amélioration de confort qui va changer le quotidien du débogage de performance. Quand tu analyses une trace dans le panneau Performance de Chrome DevTools, les événements Angular apparaissaient comme de simples entrées dans la timeline. Pour comprendre précisément à quoi correspondait chaque métrique, tu devais quitter l'inspecteur pour chercher manuellement la bonne page sur la documentation officielle.
Désormais, Angular injecte un lien hypertexte direct vers sa documentation au sein de chaque événement capturé par le profiler :
| Événement | Documentation liée |
|---|---|
ChangeDetectionStart/End |
Guide des performances runtime |
ChangeDetectionSyncStart/End |
Guide des performances runtime |
AfterRenderHooksStart/End |
afterEveryRender et afterNextRender |
DeferBlockStateStart/End |
Guide du @defer |
ngOnInit, ngOnDestroy, etc. |
Section dédiée dans le guide lifecycle |
Pour en profiter, tu as besoin de Chrome 140 minimum et de l'activation du flag expérimental suivant dans ton navigateur : chrome://flags/#enable-devtools-deep-link-via-extensibility-api.
Http : L'API fetch prend le pouvoir par défaut
Depuis Angular 2, toutes les requêtes gérées par le HttpClient passaient historiquement par XMLHttpRequest (via HttpXhrBackend). Angular 22 bascule le backend par défaut vers l'API fetch native du navigateur. Ce changement était préparé depuis Angular 18 avec withFetch(). Désormais cette fonction est dépréciée et peut être supprimée sans effet : fetch est actif d'office.
// ❌ Avant Angular 22
provideHttpClient(withFetch())
// ✅ Angular 22+ : fetch par défaut, withFetch() devient inutile
provideHttpClient()⚠️ Breaking change : le suivi de progression des uploads n'est plus supporté par défaut. L'API fetch ne permet pas de suivre la progression d'un upload côté client. Les applications qui utilisaient reportProgress: true avec une requête POST ou PUT contenant un body devront explicitement revenir sur XHR via withXhr() :
provideHttpClient(withXhr())Une migration automatique est incluse dans ng update. Elle détecte les appels à provideHttpClient() dans les applications existantes et ajoute withXhr() pour préserver le comportement actuel, sans intervention manuelle.
Scission de reportProgress : reportUploadProgress & reportDownloadProgress
Le passe par défaut à fetch a révélé un défaut de conception de l'ancienne option reportProgress : elle était binaire alors que les deux backends ont des capacités asymétriques. fetch supporte nativement le suivi des téléchargements via ReadableStream, mais ne supporte pas le suivi des uploads. Fusionner les deux dans un seul booléen rendait impossible de lever une erreur précise.
Pour clarifier la situation, l'option globale reportProgress est officiellement dépréciée. Elle est remplacée par deux options distinctes et explicites sur HttpRequest:
// Avant
this.http.post('/upload', file, { reportProgress: true, observe: 'events' })
// Après
this.http.post('/upload', file, { reportUploadProgress: true, observe: 'events' })
this.http.get('/file', { reportDownloadProgress: true, observe: 'events' })Comportement en fonction du backend choisi :
reportDownloadProgressfonctionne parfaitement, que tu utilises le moteurfetchouXHR.-
reportUploadProgress, en revanche, lève une erreur d'exécution (RuntimeError: FETCH_UPLOAD_PROGRESS_NOT_SUPPORTED) si ton application tourne sousFetchBackend. Pour l'utiliser, le retour àwithXhr()est obligatoire.
Les deux nouvelles options coexistent avec l'ancienne pendant la période de dépréciation, ce qui laisse le temps de migrer sans rien casser.
🚀 Migrations
TypeScript 6.0 devient le strict minimum
Angular 22 abandonne le support de TypeScript 5.9pour exiger au minimum TypeScript 6.0. Ce n'est pas une surprise, Angular suit de près les versions TypeScript et abandonne régulièrement les anciennes à chaque mineure.
Ce qui mérite attention ici, c'est que TypeScript 6.0 est une version majeure avec quelques changements de comportement par défaut :
- Le mode
strictest activé par défaut. - La propriété
modulebascule par défaut suresnext - La cible de compilation
targetpasse àes2025
Si ton tsconfig.json ne définit pas ces options explicitement, la mise à jour TypeScript seule peut changer le comportement de ton projet.
ng update se charge de la mise à jour de la dépendance automatiquement mais un coup d'œil au tsconfig.json avant de lancer la migration reste donc vivement conseillé !
fix(migrations): prevent trailing comma syntax errors after removing NgStyle
https://github.com/angular/angular/commit/730684b9ce8335b91ff224422fb12b7eafeaec1d
https://github.com/angular/angular/pull/67714
fix(migrations): inject migration not work in multi-project workspace with option path
https://github.com/angular/angular/commit/a73b4b7c30ae943966ad6deecf5a284cddb1f3fd
https://github.com/angular/angular/issues/66074
Protection de build : strictTemplates forcé à false
Angular pousse activement vers un typage strict des templates pour détecter les erreurs dès la phase de compilation. Cependant, activer brusquement le flag strictTemplates sur un projet existant lors d'une mise à jour majeure peut générer des centaines d'erreurs de build d'un coup, bloquant ainsi le travail des équipes.
ng update ajoute désormais "strictTemplates": false dans le tsconfig.json des projets existants. Sans ce flag explicite, un ng update pourrait déclencher une avalanche d'erreurs de compilation sur du code qui fonctionnait avant. Cela permet de figer temporairement ton comportement actuel pour migrer sereinement vers Angular 22 et l'équipe peut activer strictTemplates: true quand elle le décide, à son rythme.
currentSnapshot requis dans CanMatchFn
Le troisième paramètre, currentSnapshot (de type PartialMatchRouteSnapshot), devient obligatoire. Une migration automatique intégrée can-match-snapshot-required détecte les classes qui implémentent CanMatch avec deux paramètres et ajoute le troisième automatiquement :
// Avant
class MyGuard implements CanMatch {
canMatch(route: Route, segments: UrlSegment[]) { ... }
}
// Après (migration)
class MyGuard implements CanMatch {
canMatch(route: Route, segments: UrlSegment[], currentSnapshot: PartialMatchRouteSnapshot) { ... }
}Désactivation automatique des options pour opérateurs obsolètes
Comment on l'a vu plus haut, le compilateur sait désormais que tes variables ne sont plus nulles à l'intérieur de ces blocs conditionnels. Par conséquent, tous tes anciens ?. (optional chaining) ou ?? (nullish coalescing) restés dans le code de tes templates deviennent instantanément obsolètes et redondants.
Pour éviter de bloquer les projets lors de la mise à jour, la commande ng update va automatiquement désactiver temporairement deux options de vérification dans ton fichier tsconfig.json
nullishCoalescingNotNullableoptionalChainNotNullable
Ton application continuera de compiler normalement, te laissant le temps de nettoyer tes templates pour supprimer ces opérateurs devenus inutiles.
Migration Modèles + Outputs : Fin des collisions
Pour rappel, l'API model() génère implicitement deux éléments : la propriété de lecture (exemple: foo ) pour la valeur et une sortie d'événement fooChange pour l'événement. Si un composant déclarait déjà un output() nommé fooChange en parallèle, les deux entraient en collision.
La migration va remplacer intelligemment ton model() conflictuel par une combinaison hybride d'un input() standard couplé à un linkedSignal(), reproduisant le même comportement fonctionnel mais sans aucun conflit de nommage :
// Avant — collision : model() génère fooChange, output fooChange existe déjà
foo = model.required<number>();
fooChange = output<number>();
// Après (migration)
fooInput = input.required<number>({alias: 'foo'});
foo = linkedSignal(this.fooInput);
fooChange = output<string>();L'hydratation incrémentale s'active par défaut
Dernière excellente surprise pour clore ce chapitre sur la migration : l'hydratation incrémentale devient le comportement par défaut d'Angular . Intégrée nativement au sein de la fonction provideClientHydration(), cette fonctionnalité majeure te permet de charger et d'hydrater ton code HTML côté client de manière progressive, uniquement lorsque l'utilisateur interagit avec une zone de la page . Un atout inestimable pour les performances de tes applications à fort trafic ! Elle est accompagnée de withNoIncrementalHydration() pour désactivier l'hydratation incrémentale.
📝 Formulaires : La consécration des Signal Forms
Passage en API publique
Introduites comme expérimentales dans Angular 21, les Signal Forms sont officiellement stables en Angular 22. Toutes les APIs sous @angular/forms/signals abandonnent le tag @experimental pour le précieux sésame @publicApi.
Pour rappel, Signal Forms est une API de formulaires construite entièrement sur les signaux. À la différence des formulaires réactifs classiques (FormControl, FormGroup), elle définit un schéma de données typé, des règles de validation déclaratives, et une synchronisation automatique avec l'UI.
L'instanciation "Lazy" des champs
Quand Angular initialisait un formulaire Signal Forms, il construit un arbre de nœuds (FieldNode), un nœud par champ. Jusqu'ici, cet arbre était entièrement construit au démarrage, même pour les champs qui n'ont aucune règle de validation, aucun debounce, aucune logique.
Sur des formulaires massifs (tableaux dynamiques, fiches techniques complexes), cette surcharge de travail initiale pouvait ralentir l'affichage du composant et consommer de la mémoire inutilement.
Angular 22 corrige cela et désormais, l'arbre du formulaire ne génère ses nœuds et ses sous-champs qu'au moment précis où l'application ou le template tente d'y accéder.
Formulaire
├── name (required, minLength) ← logique → instancié immédiatement
├── bio (aucune règle) ← pas de logique → instancié à la demande
├── address
│ ├── street (aucune règle) ← pas de logique → instancié à la demande
│ └── zip (pattern) ← logique → instancié immédiatement
└── preferences (aucune règle) ← pas de logique → instancié à la demandeLe nœud de tes préférences ne sera créé en mémoire que si ton utilisateur clique sur l'onglet "Paramètres" qui affiche cette partie du formulaire. Une optimisation invisible mais massive pour la fluidité de tes applications !
touched : une propriété qui n'était pas faite pour le two-way binding
Dans les Signal Forms, les contrôles de formulaire exposaient une propriété touched utilisable en two-way binding [(touched)]. Ce choix de conception permettait à un composant enfant de "dé-toucher" un champ, c'est-à-dire remettre touched à false.
Angular 22 sépare les rôles en deux :
[touched](input) : reçoit l'état depuis le parent(touch)(output) : signale que l'utilisateur vient d'interagir avec le champ
// ❌ Avant : lecture ET écriture possible
@Component({ selector: 'my-input', ... })
class MyInput implements FormValueControl<string> {
value = model('');
touched = model(false); // two-way binding involontaire
touchIt() {
this.touched.set(true); // pouvait aussi faire .set(false) ← problème
}
}
// ✅ Après
@Component({ selector: 'my-input', ... })
class MyInput implements FormValueControl<string> {
readonly value = model('');
readonly touched = input(false); // reçoit l'état depuis le parent
readonly touch = output<void>(); // signale l'interaction, jamais l'absence
touchIt() {
this.touch.emit(); // émet un événement, ne touche plus à l'état directement
}
}L'option 'blur' débarque sur la règle de debounce
La fonction debounce() de Signal Forms accepte désormais 'blur' comme configuration, en plus d'une durée en millisecondes.
Quand tu passes 'blur', la valeur saisie par l'utilisateur ne sera poussée vers ton modèle de données que lorsque le champ perdra le focus. C'est idéal pour éviter les recalculs intempestifs sur les validations lourdes :
readonly #formSchema = schema<FormModel>((path) => {
required(path.name);
debounce(path.name, 400);
required(path.email);
debounce(path.email, 'blur');
});Validation asynchrone
Deux grosses nouveautés viennent perfectionner les contrôles asynchrones.
Le Debounce intégré sur validateAsync et validateHttp
Plus besoin de ruser pour éviter de spammer ton serveur à chaque touche pressée. Tu peux désormais configurer un paramètre debounce directement au cœur de ton validateur HTTP :
validateHttp(path.email, {
request: ({value}) => `/api/check?email=${value()}`,
onSuccess: (available: boolean) => (available ? undefined : {kind: 'email-taken'}),
onError: () => null,
debounce: 500
});reloadValidation() : forcer une revalidation
Certains validateurs asynchrone dépendent de données extérieures au champ (un état serveur, un autre champ du formulaire…), tu peux désormais appeler reloadValidation() sur un FieldState. Cela va forcer l'exécution de tous les validateurs asynchrones du nœud et de ses enfants, comme si l'utilisateur venait de modifier la valeur.
form = form({ email: field<string>() });
this.form().reloadValidation();Récupérer une erreur spécifique avec FieldState.getError()
Pour afficher tes messages d'erreur de manière élégante, tu devais souvent fouiller tout l'objet des erreurs renvoyé par le champ. FieldState expose désormais getError(kind: string).
Elle renvoie directement la première erreur du type demandé (ou undefined), et met à jour automatiquement dès que l'état change.
@let emailError = form.email().getError('email-taken');
@if (emailError) {
<p class="error">Cet email est déjà utilisé.</p>
}Lier un number|null à <input type="text">
<input type="number"> offre une UX souvent indésirable : flèches de défilement, comportements surprenants selon les navigateurs. MDN recommande recommande d'ailleurs plutôt l'usage de <input type="text" inputmode="numeric">.
Signal Forms ne savait pas convertir number|null depuis/vers une chaîne de caractères sans un intermédiaire. Angular 22 gère ce binding automatiquement. Dès que ton [formField] est lié à un modèle numérique, les règles de conversion suivantes sont appliquées :
- chaîne vide →
null - chaîne numérique valide →
number - chaîne non numérique → erreur
parseError
<input type="text" inputmode="numeric" [formField]="form.amount" />FormValueControl<T> : Adieu l'enfer de ControlValueAccessor
Pour concevoir un composant de formulaire personnalisé réutilisable comaptible avec ngModel ou formControl, il fallait jusqu'ici implémenter l'interface ControlValueAccessor et ses verbeuses méthodes writeValue(), registerOnChange(), registerOnTouched() etc. Une syntaxe complexe, très éloignée de la philosophie des Signaux.
FormValueControl<T> : une interface minimaliste
Angular 22 introduit FormValueControl<T>. Un composant qui l'implémente expose simplement un model() nommé value :
import { Component, model } from '@angular/core';
import { FormValueControl } from '@angular/forms/signals';
@Component({
selector: 'fancy-input',
template: `
<textarea [value]="value()" (input)="value.set($event.target.value)"></textarea>
`,
})
export class FancyInput implements FormValueControl<string> {
value = model<string>('');
}Ce composant est immédiatement compatible avec toutes les syntaxes de formulaires Angular :
<!-- Template forms -->
<fancy-input [(ngModel)]="myValue" />
<!-- Reactive forms -->
<fancy-input [formControl]="myControl" />
<fancy-input formControlName="email" />
<!-- Signal Forms -->
<fancy-input [formField]="state.fields.email" />Typage strict et nouvelles règles de validation
Les règles de validation min et max n'acceptent plus les chaînes de caractères (string) et requièrent uniquement types numérique number ou des objets de type Date.
Pour les dates, de nouvelles règles explicites minDate et maxDate font leur apparition et se chargent de positionner les attributs HTML min/max correctement formatés sur les éléments <input type="date"> et <input type="month">.
required(path.birthday);
minDate(path.birthday, new Date('1998-05-11'));
maxDate(path.birthday, new Date());ngNoCva : désactiver le fallback ControlValueAccessor
Si un composant implémente FormValueControl mais qu'il hérite d'un ControlValueAccessor (via une bibliothèque tierce ou un composant parent), Angular pourrait se tromper et brancher le mauvais protocole. La directive ngNoCva désactive ce fallback explicitement :
<input ngNoCva [field]="myWonderfulField">En vrac : Les petites corrections qui font du bien
[formRoot] sans gestion de soumission. La directive [formRoot] levait une erreur si le formulaire n'avait pas de configuration submit. Elle se contente maintenant de poser novalidate et d'intercepter le submit natif, sans appeler quoi que ce soit.
Support des NG_VALIDATORS en mode CVA. Les validateurs Angular classiques (NG_VALIDATORS) posés en externe sur un composant FormValueControl sont désormais pris en charge nativement dans le système parseErrors de Signal Forms. Les deux mondes coexistent sans configuration supplémentaire.
Signals : Une réactivité sous contrôle
L'écosystème des Signaux continue de s'enrichir pour combler les derniers cas d'usage qui nécessitaient encore l'utilisation de RxJS. Angular 22 franchit un cap majeur avec la gestion native du temps.
L'utilitaire debounced() : Temporise tes Signaux sans RxJS
Jusqu'à présent, lorsqu'on voulait retarder la prise en compte d'un changement sur un Signal (par exemple, attendre que l'utilisateur arrête de taper dans un champ de recherche avant de lancer une requête lourde), le framework n'offrait pas de solution native.
Tu étais obligé de faire un aller-retour fastidieux vers RxJS en convertissant ton Signal en Observable, en lui appliquant l'opérateur debounceTime, pour ensuite le ré-envelopper dans un Signal :
// ❌ Avant Angular 22
readonly searchQuery = signal('');
searchQuery$ = toObservable(this.searchQuery).pipe(debounceTime(1000));
debouncedSearchQuery = toSignal(this.searchQuery$);Avec l'introduction de la fonction utilitaire debounced(), plus besoin d'importer la moindre ligne de RxJS pour temporiser tes données!
Cette fonction prend ton Signal d'origine, lui applique un délai d'attente (en millisecondes), et génère une version temporisée. Sous le capot, le résultat est représenté sous la forme d'une Resource, ce qui te permet de suivre facilement son état (si le debounce est en cours, etc.) :
// ✅ Avec Angular 22
readonly searchQuery = signal('');
debouncedSearchQuery = debounced(this.searchQuery, 1000);
Tests : Un outillage de plus en plus affûté
TestBed.getLastFixture() : Accède instantanément à ton composant
Lors de l'écriture de tests d'intégration complexes, il arrive fréquemment que tu doives instancier un composant, puis récupérer sa "fixture" (l'objet qui permet de manipuler le composant et de déclencher la détection de changement) au sein de fonctions utilitaires ou de sous-blocs it.
Jusqu'ici, tu étais obligé de déclarer une variable globale au bloc describe et de lui assigner manuellement la valeur à chaque création :
// ❌ Avant Angular 22
describe('MyComponent', () => {
let component: MyComponent;
let fixture: ComponentFixture<MyComponent>;
beforeEach(() => {
fixture = TestBed.createComponent(MyComponent);
component = fixture.componentInstance;
});
function clickSubmitButton() {
const button = fixture.nativeElement.querySelector('button');
button.click();
fixture.detectChanges();
}
it('should submit successfully', () => {
clickSubmitButton();
// ...
});
});La nouvelle méthode TestBed.getLastFixture() te permet de récupérer instantanément la fixture du composant le plus récemment créé au sein de ton contexte de test, sans avoir à la stocker dans une variable locale :
// ✅ Avec Angular 22
describe('MyComponent', () => {
beforeEach(() => {
TestBed.createComponent(MyComponent);
});
function clickSubmitButton() {
const fixture = TestBed.getLastFixture();
const button = fixture.nativeElement.querySelector('button');
button.click();
fixture.detectChanges();
}
it('should submit successfully', () => {
clickSubmitButton();
// ...
});
});🛣️ Router : Navigation fine et bindings avancés
Le Routeur d'Angular continue de s'affiner dans cette version, notamment pour offrir un contrôle total sur ce qui s'affiche dans la barre d'adresse du navigateur et pour sécuriser le transfert de données vers tes composants.
L'input browserUrl : Personnalise l'affichage de ta barre d'adresse
C'est une excellente nouveauté pour l'expérience utilisateur (UX). Jusqu'ici, la directive routerLink gérait de manière indissociable le composant à charger et l'URL à afficher dans le navigateur.
La nouvelle propriété browserUrl te permet de séparer complètement la route interne du rendu visuel :
<a routerLink="/catalog/products/detail/42" [browserUrl]="/my-wonderfull-product">
Voir le produit
</a>Lorsque l'utilisateur clique sur ce lien, Angular va bien déclencher la navigation interne vers la route technique /catalog/products/detail/42 pour charger le bon composant, mais il va instantanément mettre à jour l'URL du navigateur pour afficher /my-wonderfull-product.
withComponentInputBinding() s'arme d'un paramètre d'options
Depuis Angular 16, la fonction withComponentInputBinding() permet d'associer automatiquement les paramètres d'une route (les paramètres de chemin, les query params ou les données du bloc data) directement à des input() dans tes composants. Angular 22 fait évoluer cette API en lui ajoutant un objet de configuration optionnel pour te donner un contrôle sur ces liaisons.
L'option queryParams te permet de spécifier si les paramètres de requête (ce qui se trouve après le ? dans l'URL) doivent être injectés comme des entrées du composant.
true(par défaut) : Le routeur lie automatiquement toutes les variables de l'URL à tes inputs.false: Angular ignore totalement les query params, te permettant d'isoler uniquement les paramètres de chemin stricts.
De son côté, l'option unmatchedInputBehavior dicte la manière dont le framework doit réagir lorsqu'un composant possède un input(), mais que la route active n'envoie aucun paramètre correspondant. Tu as le choix entre deux comportements très précis :
'alwaysUndefined'(par défaut) : Angular injecte systématiquementundefinedà l'entrée du composant si le routeur n'a rien trouvé à lui transmettre.'undefinedIfStale': Angular va injecterundefineduniquement si cette entrée était présente et alimentée par le routeur lors de la route précédente. Si l'entrée n'a jamais eu vocation à être définie par le routeur, Angular n'y touche pas, préservant ainsi sa valeur par défaut ou son état interne initial.
Voici comment intégrer proprement ces options dans ton fichier de configuration globale :
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withComponentInputBinding({
queryParams: false, // Désactive la liaison automatique des query params (?search=...)
unmatchedInputBehavior: 'undefinedIfStale' // Protège les valeurs par défaut de tes composants
})
)
]
};Et voici l'impact concret au cœur de ton composant de destination :
@Component({ ... })
export class ProductDetailComponent {
// Ce paramètre provient du chemin strict 'product/:id' -> Toujours mappé correctement
id = input.required<string>();
// Un état interne propre au composant (sans rapport avec le routeur).
// Grâce à 'undefinedIfStale', Angular ne viendra pas l'écraser avec un 'undefined'
// lors du chargement initial de la route !
isMenuOpen = signal(false);
}🏁 En bref : On migre ou on attend ?
Bilan des courses : Angular 22 ne fait pas semblant. Ce n'est pas une simple mise à jour de confort, c'est le moment où toutes les briques de ces dernières années s'emboîtent enfin. Avec OnPush par défaut, les Signal Forms stables et l'utilitaire injectAsync, le message est clair : le framework est désormais Signals-First. Grâce à un ng update bien rodé qui fige l'ancien comportement, tu peux sauter le pas sereinement et nettoyer ton code à ton propre rythme.
🔮 Et après ?
La suite s'annonce tout aussi intéressante. Pendant que l'intégration native de WebMCP prépare discrètement ton architecture actuelle à devenir un ensemble d'outils typés directement pilotables par des agents IA , Angular 22 prouve qu'il a atteint la maturité nécessaire pour de la grosse production.
Le terrain de jeu est propre, moderne et ultra-performant. Alors, tu lances le ng update quand ?