Aller au contenu

Angular 22 : le point de non-retour vers une architecture Signals-First

Sortie le 03 juin 2026, la version 22 d'Angular marque un tournant décisif : passage à OnPush par défaut, Signal Forms prêts pour la production, instanciation lazy et outils de débogage assistés par l'IA. Que tu sois junior ou senior, découvre tout ce qui change aujourd'hui dans ton code !

Découvrez la version 22 d'Angular !

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.

refactor(core): Add migration to add `ChangeDetectionStrategy.Eager` … by JeanMeche · Pull Request #67056 · angular/angular
…where applicable In v22, OnPush becomes the default strategy. To maintain the ChangeDetection behavior of exisiting apps, components without an explicit change detectino strategy will get Eager as…

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 !

feat(core): bootstrap via `ApplicationRef` with config by JeanMeche · Pull Request #67948 · angular/angular
This is to align the shape of the method with createComponent BREAKING CHANGE=The second arguement of appRef.bootstrap does not accept any anymore. Make sure the element you pass is not nullable. f…

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 DropdownItemComponent

Angular 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.

feat(core): de-duplicate host directives by crisbeto · Pull Request #67996 · angular/angular
With host directives we can end up in a situation where the same directive applies multiple times to the same element, potentially with conflicting configurations. The runtime isn't set up for…

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 un Promise<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 !

feat(core): Add an `injectAsync` helper function by JeanMeche · Pull Request #68248 · angular/angular
The commit introduces a new function to assist users who want to lazy load services and use the DI system to create them. Example: import {injectAsync} from &#39;angular/core&#39;; class MyCmp {…

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 idleloading 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),
});
feat(core): add special return statuses for resource params by mmalerba · Pull Request #67084 · angular/angular
Allows throwing from the resource&#39;s params function to transition the resource to a status other than resolved. In particular, the following values can be thrown from params: ResourceParamsSta…

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 :

  1. En SSR, Angular pré-génère le DOM côté serveur et le transfère au client
  2. 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.
  3. 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é.

feat(core): allow synchronous values for stream Resources by JeanMeche · Pull Request #67382 · angular/angular
In order for resources to allow caching in SSR context (eg in the TransferState), resource need to be able to set their value synchronously. If the resource value is not set synchronously, the reso…

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
});
feat(core): add ability to cache resources for SSR by JeanMeche · Pull Request #68226 · angular/angular
This commit adds a transferCacheKey option to enable easy caching for resource/ rxResource. By caching resource data we make sure that resources are not in a loading state during hydration on the c…

🤖 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.

Exemple signal graph puis Angular DevTools
Add DI graph AI debugging tool by dgp1130 · Pull Request #68030 · angular/angular
This is part of an experiment with chrome-devtools-mcp to expose runtime data about framework internal state such as the dependency injection graph to AI agents and see if it improves debuggability…

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 ?".

Add signal graph AI debugging tool by dgp1130 · Pull Request #67985 · angular/angular
This is part of an experiment with chrome-devtools-mcp to expose runtime data about framework internal state such as the signal graph to AI agents and see if it improves debuggability of Angular ap…

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' }],
        }),
      },
    ]),
  ],
};
0:00
/0:20

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'outil search_products
  • Sur /cart, il verra uniquement l'outil checkout.

Les outils entrent et sortent du registre de l'IA de manière totalement transparente, sans aucune ligne de nettoyage manuel de ta part !

Initial WebMCP Support by dgp1130 · Pull Request #68139 · angular/angular
This PR adds declareWebMcpTool and provideWebMcpTools as new, experimental public APIs. declareWebMcpTool is a mechanism for registering WebMCP tools and tying them to Angular&#39;s Injector lifecy…

🎨 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>
  }
Support optional timeout for idle deferred triggers by SkyZeroZx · Pull Request #67190 · angular/angular
This change allows supply a timeout value in the on idle trigger of an Angular @defer block, which translates into passing the timeout option to requestIdleCallback. That way, if the browser doesn’…

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)
  ]
};
feat(core): support customization of @defer’s on idle behavior by thePunderWoman · Pull Request #67262 · angular/angular
This commit makes the behavior of on idle in @defer configurable via DI. It defines an IdleService interface that an application can implement and provide to Angular: @Injectable({providedIn: &#39;…

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.

feat(compiler): Support comments in html element. by JeanMeche · Pull Request #67179 · angular/angular
Let’s support comments ! <div // comment 0 /* comment 1 */ attr1=“value1” /* comment 2 spanning multiple lines */ attr2=“value2″ ></div>

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 !

feat(core): Allow other expression for exhaustive typechecking by JeanMeche · Pull Request #67748 · angular/angular
When the switched expression is nested within a union, exhaustive typechecking needs to know which expression to check. This change adds the possibility of specifying the expression to check: @Comp…

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.
feat(language-service): add Angular-specific Inlay Hints (LSP 3.17) by kbrilla · Pull Request #66755 · angular/angular
PR Checklist The commit message follows our guidelines Tests for the changes have been added Docs have been added / updated PR Type Feature Refactoring (small infrastructure extraction to s…
  • 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.
feat(language-service): add Document Symbols support for Angular templates by kbrilla · Pull Request #66690 · angular/angular
PR: feat(language-service): add Document Symbols support for Angular templates Description Adds comprehensive Document Symbols support for Angular templates, enabling the Outline panel, breadcrumbs…

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 !

fix(compiler): allow safe navigation to correctly narrow down nullables by JeanMeche · Pull Request #67959 · angular/angular
The commit updates the TCB for safe navigation expressions to allow for correct narrowing of nullables. This will trigger the nullishCoalescingNotNullable and optionalChainNotNullable diagnostics o…

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.

feat(compiler): Angular expressions with optional chaining returns `undefined` by JeanMeche · Pull Request #68084 · angular/angular
To mitigate this breaking change, this behavior can be disabled by wrapping expressions with the $safeNavigationMigration magic function. : $safeNavigationMigration(foo?.bar?.baz) fixes #34385, #3…

🛠️ 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.

feat(core): enhance profiling with documentation URLs by SkyZeroZx · Pull Request #67942 · angular/angular
Enhances the Chrome DevTools performance profiling integration by adding links to relevant Angular documentation for lifecycle hooks and profiler events. Closes #63959 Second attempt at #65606 , wh…

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.

feat(http): Use the Fetch backend by default by JeanMeche · Pull Request #58212 · angular/angular
This commit replaces the HttpXhrBackend with the FetchBackend as the default implementation of the HttpBackend. This introduces a breaking change a the FetchBackend does not support the report prog…

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 :

  • reportDownloadProgress fonctionne parfaitement, que tu utilises le moteur fetch ou XHR.
  • reportUploadProgress, en revanche, lève une erreur d'exécution ( RuntimeError: FETCH_UPLOAD_PROGRESS_NOT_SUPPORTED) si ton application tourne sous FetchBackend. 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.

refactor(http): Add `reportUploadProgress` & `reportDownloadProgre… by JeanMeche · Pull Request #68495 · angular/angular
…ss` options In order to raise an error on upload progress on the FetchBackend, we split reportProgress into 2 respective properties. DEPRECATION: The reportProgress option is deprecated please use…

🚀 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 strict est activé par défaut.
  • La propriété module bascule par défaut sur esnext
  • La cible de compilation target passe à 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é !

feat(core): drop support for TypeScript 5.9 by crisbeto · Pull Request #68009 · angular/angular
Drops support for TypeScript 5.9. BREAKING CHANGE: TypeScript versions older than 6.0 are no longer supported.

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): prevent trailing comma syntax errors after removing NgStyle by aparzi · Pull Request #67714 · angular/angular
This fixes an issue where when removing NgStyle from the imports array of a component, an extra trailing comma would be left behind if it was the last element in that component`. PR Checklist Pleas…

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

Inject migration does not work in a multi-project workspace · Issue #66074 · angular/angular
Which @angular/* package(s) are the source of the bug? core Is this a regression? No Description In multi-project workspace, the inject migration doesn’t work when targeting one of the projects due…

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.

feat(migrations): add strictTemplates to tsconfig during ng update by aparzi · Pull Request #67884 · angular/angular
Add the strictTemplates option to tsconfig.json with a default value of false PR Checklist Please check if your PR fulfills the following requirements: The commit message follows our guidelines:…

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) { ... }
}
fix(router): make currentSnapshot required in CanMatchFn by atscott · Pull Request #67452 · angular/angular
it was only optional to avoid a breaking change in a minor BREAKING CHANGE: The currentSnapshot parameter in CanMatchFn and the canMatch method of the CanMatch interface is now required. While this…

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

  • nullishCoalescingNotNullable
  • optionalChainNotNullable

Ton application continuera de compiler normalement, te laissant le temps de nettoyer tes templates pour supprimer ces opérateurs devenus inutiles.

feat(migrations): Disabling nullishCoalescingNotNullable & optionalChainNotNullable on ng update by aparzi · Pull Request #68080 · angular/angular
Related to #67959 disabling two diagnostics errors by ng update: nullishCoalescingNotNullable optionalChainNotNullable PR Checklist Please check if your PR fulfills the following requirements:…

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>();
feat(migrations): model + output migrations by aparzi · Pull Request #67349 · angular/angular
becomes input + linkedSignal fixes #67340 PR Checklist Please check if your PR fulfills the following requirements: The commit message follows our guidelines: https://github.com/angular/angular/b…

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.

feat(platform-browser): make incremental hydration default behavior by thePunderWoman · Pull Request #68092 · angular/angular
This PR automatically enables incremental hydration by default in provideClientHydration(). It introduces a new withNoIncrementalHydration() feature for opting out, implements developmental conflic…

📝 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 (FormControlFormGroup), elle définit un schéma de données typé, des règles de validation déclaratives, et une synchronisation automatique avec l'UI.

feat(forms): graduate signal forms APIs to public API by alxhub · Pull Request #68581 · angular/angular
Replaced @experimental tags with @publicApi 22.0 across all Signal Forms APIs under packages/forms/signals to mark them as ready for general use in v22.

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 demande

Le 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 !

perf(forms): lazily instantiate signal form fields by alxhub · Pull Request #67344 · angular/angular
Currently, Signal Forms eagerly instantiates all nodes in the form tree because childrenMap iterates over the value and creates a FieldNode for every property. This ensures validation side-effects…

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
  }
}
fix(forms): split the `touched` model into an input and `touch` output by leonsenft · Pull Request #67100 · angular/angular
The touched property was never meant to support two-way binding; a control should not be able to dictate that a field is no longer touched. The touched input represents the touched state of the fi…

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');
});
feat(forms): add ‘blur’ option to debounce rule by leonsenft · Pull Request #67418 · angular/angular
Expands the debounce rule configuration to accept &#39;blur&#39;. When this option is provided, the rule will delay model synchronization until the field loses focus (is touched). This introduces a…

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
});
feat(forms): add debounce option to validateAsync and validateHttp by alxhub · Pull Request #67813 · angular/angular
This adds support for a debounce option to the validateAsync and validateHttp functions. This allows developers to debounce the triggering of async validators to improve performance. A DebounceTime…

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();
feat(forms): add `reloadValidation` to Signal Forms to manually trigger async validation by alxhub · Pull Request #67360 · angular/angular
This commit introduces a formal mechanism to manually re-trigger asynchronous validations in Signal Forms, addressing #66994. It exposes a reloadValidation method on the FieldState interface that r…

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>
}
feat(forms): add FieldState.getError() by alxhub · Pull Request #67811 · angular/angular
Added a getError(kind: string) method to FieldState that returns the first validation error of a given kind, or undefined if no such error exists. This method is reactive and will re-evaluate when…

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" />
feat(forms): support binding `number|null` to `<input type=“text”>` by alxhub · Pull Request #67268 · angular/angular
&lt;input type=&quot;number&quot;&gt; often does not provide the desired user experience when editing numbers in a form. MDN even describes how text inputs should be used in many cases instead, via…

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" />
Signal Forms: support FormUiControl in Template & Reactive Forms by alxhub · Pull Request #67267 · angular/angular
In this PR, we implement support in template &amp; reactive forms for the new style of Signal Forms control: @Component({ selector: &#39;fancy-input&#39;, template: ` &lt;textarea [value]=&…

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());
[Signal Forms] Update `min` and `max` to operate on `number` and `Date` values by leonsenft · Pull Request #68001 · angular/angular
Remove string support from min and max validation rules. This is no longer necessary since FormField can bind a numeric field to a text based control. Add minDate and maxDate validation rules. Thes…

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">
Signal Forms: support FormUiControl in Template & Reactive Forms by alxhub · Pull Request #67267 · angular/angular
In this PR, we implement support in template &amp; reactive forms for the new style of Signal Forms control: @Component({ selector: &#39;fancy-input&#39;, template: ` &lt;textarea [value]=&…

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.

fix(forms): allow FormRoot to be used without submission options by leonsenft · Pull Request #67727 · angular/angular
The [formRoot] directive will no longer call submit() if the bound form doesn&#39;t define its own submission options. This allows the directive to be used solely for the default behavior it provid…

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.

Support NG_VALIDATORS in CVA mode by alxhub · Pull Request #67943 · angular/angular
fix(forms): use controlValue in NgControl for CVA interop feat(forms): shim legacy NG_VALIDATORS into parseErrors for CVA mode

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);
feat(core): allow debouncing signals by mmalerba · Pull Request #67044 · angular/angular
Adds a utility debounced to create a debounced version of a signal, represented as a Resource. The resource&#39;s value contained the debounced value of the signal, while its status (resolved, load…

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();
    // ...
  });
});
feat(core): TestBed.getFixture -> TestBed.getLastFixture and update i… by atscott · Pull Request #67483 · angular/angular
…mplementation TestBed.getFixture is now TestBed.getLastFixture and returns the last fixture created rather than throwing if there are more than 1.

🛣️ 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.

feat(router): adds browserUrl input support to router links by SkyZeroZx · Pull Request #67228 · angular/angular
Second attempt at #66840 , which was previously reverted.

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ématiquement undefined à l'entrée du composant si le routeur n'a rien trouvé à lui transmettre.
  • 'undefinedIfStale' : Angular va injecter undefined uniquement 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); 
}
feat(router): Add options optional parameter for with component input binding by JordanW9232 · Pull Request #67408 · angular/angular
PR Checklist Please check if your PR fulfills the following requirements: The commit message follows our guidelines: https://github.com/angular/angular/blob/main/contributing-docs/commit-message-…
feat(router): add unmatchedInputBehavior option to componentInputBinding by atscott · Pull Request #68373 · angular/angular
Introduce a new configuration option unmatchedInputBehavior to the componentInputBinding feature. This option allows users to configure the behavior when a component input is not matched by any key…

🏁 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 ?

Dernier