AngularAtlaséd. 22.2

Chap. 13Architecture à grande échelle58 / 75

Web Components avec Angular Elements

createCustomElement : un composant Angular utilisé comme balise HTML n’importe où, plus la communication entre applications via des événements et BroadcastChannel.

stable ~0 min de lecture Documentation officielle

Un custom element (Web Component) est une balise HTML que vous définissez vous-même, <atlas-rating>, et que le navigateur reconnaît partout : dans une page statique, dans un CMS, dans une application React ou dans une autre application Angular. @angular/elements en est le pont : createCustomElement() transforme un composant Angular en une classe qui étend HTMLElement. Les inputs deviennent des attributs et des propriétés, les outputs deviennent des CustomEvents, et le composant se crée tout seul quand la balise entre dans le DOM.

Les exemples de cette page sont réels : nous définissons les éléments à l’exécution avec customElements.define, les créons avec document.createElement dans un conteneur qu’Angular ne gère pas, et les écoutons avec addEventListener, exactement comme le ferait une page qui ne sait rien d’Angular. Tout s’exécute dans cette application, qui est zoneless.

En bref
  • createCustomElement transforme un composant en Web Component utilisable sur n’importe quelle page : React, Vue, un CMS ou du HTML simple.
  • Les inputs deviennent des propriétés et des attributs (en kebab-case), et les outputs des événements CustomEvent.
  • Avec le Shadow DOM, les styles de l’élément et ceux de la page ne se mélangent plus.
  • Chaque élément apporte le runtime Angular. Plusieurs éléments sur la même page devraient venir du même bundle.
  • Cela vaut la peine pour des widgets intégrés à des pages que vous ne contrôlez pas, pas pour une application Angular classique.
Pourquoi c’est important

Angular Elements est le bon outil quand un composant doit vivre en dehors d’une application Angular : widgets pour un CMS ou pour les sites de clients, design system utilisé par des équipes sur différents frameworks, micro frontends qui ne peuvent pas partager la même version d’Angular. Dans une application Angular ordinaire, c’est presque toujours le mauvais choix : vous perdez la vérification des types dans le template, la hiérarchie de DI et la projection de contenu dynamique.

Ce que fait createCustomElement

createCustomElement(Component, { injector }) n’affiche rien. Il renvoie une classe (NgElementConstructor) que vous enregistrez avec customElements.define(tag, class). La classe lit les métadonnées du composant (reflectComponentType) et prépare trois choses :

  • observedAttributes : les noms des inputs en kebab-case (maxStars → max-stars), pour que le navigateur la notifie à chaque changement d’attribut ;
  • un getter et un setter sur le prototype pour chaque input, pour que el.maxStars = 7 fonctionne comme une propriété DOM ordinaire ;
  • une stratégie (NgElementStrategy) qui, au connectedCallback, appelle createComponent avec l’élément comme hostElement, attache la vue à ApplicationRef et exécute un premier detectChanges(), de façon synchrone, avant le retour de append().

L’injecteur que vous passez est celui où le composant obtient ses services (HttpClient, stores, tokens de configuration). Dans un bundle dédié aux widgets, vous l’obtenez depuis createApplication(), une application sans composant racine ; dans une application existante, depuis ApplicationRef.injector.

import {
  Component,
  ViewEncapsulation,
  booleanAttribute,
  computed,
  input,
  linkedSignal,
  numberAttribute,
  output,
} from '@angular/core';

export interface RatingChange {
  value: number;
  previous: number;
}

@Component({
  selector: 'atlas-rating-cmp',               // ≠ le nom de la balise (voir plus bas)
  encapsulation: ViewEncapsulation.ShadowDom, // le CSS de la page hôte n’entre pas
  template: `
    @for (s of stars(); track s) {
      <button type="button" [class.on]="s <= current()" [disabled]="readonly()"
              (click)="pick(s)">★</button>
    }
  `,
})
export class AtlasRating {
  // attributs : value, max-stars, readonly
  readonly value = input(0, { transform: numberAttribute });
  readonly maxStars = input(5, { transform: numberAttribute });
  readonly readonly = input(false, { transform: booleanAttribute });

  // devient un CustomEvent 'ratingChange', la valeur dans event.detail
  readonly ratingChange = output<RatingChange>();

  // état local, réinitialisé quand l’hôte envoie une autre valeur
  protected readonly current = linkedSignal(() => this.value());
  protected readonly stars = computed(() =>
    Array.from({ length: this.maxStars() }, (_, i) => i + 1),
  );

  protected pick(value: number): void {
    const previous = this.current();
    this.current.set(value);
    this.ratingChange.emit({ value, previous });
  }
}
La balise n’est pas le selector

Le guide officiel demande explicitement de ne pas utiliser le selector du composant comme nom de balise. Si atlas-rating est à la fois le selector et le custom element, et que le composant est aussi importé dans un template, Angular crée deux instances sur le même élément. D’où la convention atlas-rating-cmp (selector) contre atlas-rating (balise). La balise doit contenir un tiret, et define() sur un nom déjà enregistré lève NotSupportedError : c’est pourquoi on vérifie d’abord avec customElements.get().

Zone.js n’est pas nécessaire. Depuis la v19, les éléments utilisent le scheduler de Change Detection ordinaire (changelog 19.0.0 : « switch to ComponentRef.setInput & remove custom scheduler »), et depuis la v21 les applications sont zoneless par défaut. Un setAttribute atteint componentRef.setInput(), qui marque la vue et planifie un rendu ; un clic dans le template de l’élément modifie un signal, exactement comme dans un composant OnPush ordinaire.

Définir dans une application existante

Le cas typique : l’application reçoit du HTML d’un CMS et l’insère avec innerHTML. Les composants Angular de ce HTML ne démarreraient jamais, mais les custom elements démarrent tout seuls, car le navigateur reconnaît leur balise.

// Dans une application existante (p. ex. du HTML venant d’un CMS, inséré avec innerHTML)
import { ApplicationRef, inject, provideAppInitializer } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { AtlasRating } from './rating';

function defineAtlasElements(): void {
  const injector = inject(ApplicationRef).injector; // racine : vit aussi longtemps que l’application
  // define() sur un nom déjà enregistré lève NotSupportedError
  if (!customElements.get('atlas-rating')) {
    customElements.define('atlas-rating', createCustomElement(AtlasRating, { injector }));
  }
}

export const appConfig = {
  providers: [provideAppInitializer(defineAtlasElements)],
};

Attributs, propriétés, événements

Un élément a deux « entrées » pour un même input. L’attribut (max-stars="7") est toujours une string : c’est pourquoi les inputs numériques ou booléens ont besoin de transform: numberAttribute / booleanAttribute ; sans cela, maxStars() + 1 devient '71'. La propriété (el.maxStars = 7) accepte n’importe quel type : objets et tableaux ne passent donc que par là. En lecture, el.maxStars renvoie la valeur courante de l’input ; depuis la v21, c’est aussi vrai pour input() sur les signals (auparavant, il fallait appeler el.maxStars()).

Input / outputAttribut HTMLPropriété JSÀ retenir
maxStars
transform: numberAttribute
max-stars="7"el.maxStars = 7l’attribut arrive sous la forme '7', le transform en fait un nombre
readonly
transform: booleanAttribute
readonlyel.readonly = trueremoveAttribute envoie null → false
userId
alias: 'user'
user="42"el.userIdl’attribut utilise l’alias, la propriété le nom dans la classe
items
input<Item[]>
—el.items = [...]via un attribut, il arriverait sous la forme "[object Object]"
ratingChange
output<T>()
—addEventListener('ratingChange', …)CustomEvent, la valeur dans event.detail
export class Widget {
  // l’attribut = le nom de l’input (ou son alias) en kebab-case
  readonly maxStars = input(5, { transform: numberAttribute });     // max-stars="7"
  readonly compact = input(false, { transform: booleanAttribute }); // compact
  readonly userId = input('', { alias: 'user' });                   // user="42" (!)
  readonly items = input<Item[]>([]);                               // propriété uniquement

  // un output devient un CustomEvent portant le nom de l’output (ou l’alias)
  readonly ratingChange = output<RatingChange>();           // 'ratingChange'
  readonly closed = output<void>({ alias: 'atlasClosed' }); // 'atlasClosed'
}

// Ce que createCustomElement génère pour la classe ci-dessus :
//   static observedAttributes = ['max-stars', 'compact', 'user', 'items']
//   getter/setter sur le prototype :  maxStars, compact, userId, items
//   (la propriété utilise le nom dans la classe, l’attribut utilise l’alias)

L’événement est créé avec new CustomEvent(name, { detail }), sans bubbles et sans composed. Un listener sur document ou sur le conteneur parent ne reçoit rien ; on écoute directement sur l’élément. Dans l’exemple ci-dessous, le conteneur a aussi un listener pour ratingChange : il n’apparaîtra jamais dans le journal.

LiveVrai élément : <atlas-rating> défini à l’exécution
Page hôte (code DOM pur)
Envoyer en tant que
// cliquez sur un bouton ou modifiez un champ
Un conteneur qu’Angular ne gère pas
dans le DOM : noncomposants créés : 0détruits : 0lire el.value: —
Ce qui se passe (addEventListener + callbacks)
  • 09:11:33 customElements.define('atlas-rating', …) : défini maintenant
Modifiez les champs en mode « attribut », puis en mode « propriété », et suivez la commande envoyée et ce que lit el.value. Cliquez sur les étoiles : le journal affiche event.detail et bubbles=false. « Déplacer » conserve l’état ; el.remove() suivi de « append(même instance) » montre le piège décrit plus bas.

Remarquez un détail : après un clic sur une étoile, el.value reste la valeur reçue de l’hôte. L’input est un canal à sens unique ; le composant garde le choix dans un linkedSignal et l’annonce via un événement. Si l’hôte veut être la source de vérité, il écoute l’événement et réécrit el.value : le pattern du « composant contrôlé ».

// The event is created as new CustomEvent(name, { detail })
// → bubbles: false, composed: false.
el.addEventListener('ratingChange', (e: Event) => {
  const { value } = (e as CustomEvent<RatingChange>).detail;
});

document.addEventListener('ratingChange', handler); // ✗ ne reçoit rien

// Si l’hôte a vraiment besoin de délégation (un listener pour 50 widgets),
// émettez explicitement, depuis le composant, un événement qui remonte et traverse le shadow root :
export class AtlasRating {
  private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);

  private announce(detail: RatingChange): void {
    this.host.nativeElement.dispatchEvent(
      new CustomEvent('atlas:rating-change', { detail, bubbles: true, composed: true }),
    );
  }
}

Cycle de vie

Le composant naît au connectedCallback et meurt au disconnectedCallback, mais pas immédiatement : la stratégie planifie destroy() après 10 ms (DESTROY_DELAY = 10 dans le code source du package). Si l’élément revient dans le DOM avant, par exemple lors d’un déplacement remove() + append() dans la même tâche, comme au tri d’une liste, la destruction est annulée et l’état interne subsiste. Les inputs définis avant la connexion sont mémorisés et appliqués à la création. De plus, les propriétés définies sur un élément pas encore défini (le script du widget n’est pas chargé) sont reprises à l’upgrade : l’hôte peut donc définir el.items à tout moment.

document.createElement('atlas-rating')
    constructor ; pas encore de composant (les inputs définis maintenant sont mémorisés)
container.append(el)
    connectedCallback → createComponent(...) DE FAÇON SYNCHRONE, attachView(appRef), detectChanges()
el.setAttribute('value', '4')
    attributeChangedCallback → componentRef.setInput('value', '4') → transform → CD
el.value = 4
    setter sur le prototype → même chemin, sans conversion en string
el.remove()
    disconnectedCallback → destroy PLANIFIÉ dans 10 ms
    ↳ réajouté en < 10 ms (déplacement)  destroy annulé : même instance, même état
    ↳ après 10 ms                         componentRef.destroy() → DestroyRef.onDestroy
container.append(el)   // même instance, après destroy
    ⚠︎ le composant n’est PAS recréé : HTML mort + NG0953 dans la console
document.createElement('atlas-rating')
    ✓ instance neuve et propre
Rattacher un élément déjà détruit

Le guide officiel avertit (section Limitations, issue #38778) que détacher puis rattacher un élément créé avec @angular/elements pose problème. En v22.2.2, le comportement est visible dans la démo : une fois le composant détruit, un nouvel append de la même instance ne le recrée pas. Le HTML resté dans le shadow root est mort (les clics ne font rien), el.value vaut undefined, et Angular signale NG0953 (abonnement à un OutputRef détruit). Une seconde tentative lève NotSupportedError depuis attachShadow, car un élément ne peut pas avoir deux shadow roots.

Cela arrive quand quelqu’un réutilise le nœud DOM : le ng-if d’AngularJS, un éditeur de CMS qui déplace des blocs, un cache d’éléments dans votre code. La solution : ne réutilisez pas l’instance, créez un autre élément avec document.createElement, ou, si vous voulez seulement le masquer, laissez-le dans le DOM avec l’attribut hidden.

Shadow DOM et styles

Avec Emulated (le défaut), Angular réécrit les selectors du composant avec des attributs uniques : vos styles ne fuient pas vers l’extérieur, mais le CSS de la page entre. Un widget placé sur un site tiers hérite de toute règle comme .content p ou button { text-transform: uppercase !important }, le bug le plus courant des widgets. Avec ViewEncapsulation.ShadowDom, le template est rendu dans un shadow root (mode open), et les selectors de la page ne voient plus l’intérieur. Seules les propriétés héritées (font, color) et les variables CSS traversent, exactement ce qu’il faut pour un thème contrôlé.

Une nuance peu connue : en mode ShadowDom, Angular copie aussi dans chaque shadow root les styles des autres composants de la même application. Pour les composants Emulated, c’est du poids mort (leurs selectors ne correspondent pas), mais un composant avec ViewEncapsulation.None finit par styler aussi l’intérieur du widget. ExperimentalIsolatedShadowDom (expérimental depuis la v21) place uniquement les styles propres du composant dans le shadow root. Pendant le SSR, les deux modes shadow sont rendus comme Emulated.

EncapsulationLe CSS de la page entre-t-il ?Styles None de l’applicationVariables CSS
Emulated (défaut)ouiouioui
ShadowDomnonoui (copiés dans le shadow root)oui
ExperimentalIsolatedShadowDomexpérimentalnonnonoui
LiveLe même widget, trois encapsulations
Élémentel.shadowRoot<style> dans le shadow root
Cochez les cases une par une. Le CSS du « CMS » ne casse que la variante Emulated. Le composant None atteint aussi ShadowDom (le nombre d’éléments style augmente d’un), mais pas la variante isolée. La variable CSS applique le thème aux trois.

Le Shadow DOM change plus que les styles : les événements qui quittent un shadow root sont retargetés (event.target devient l’hôte), document.querySelector ne trouve rien de l’intérieur, et les tests e2e doivent percer le shadow root. Pour le thème, exposez des variables CSS et, si vous avez besoin de plus de contrôle, des attributs part stylables avec ::part().

@Component({
  selector: 'atlas-card-cmp',
  encapsulation: ViewEncapsulation.ShadowDom,
  template: `
    <h3 part="title">{{ title() }}</h3>
    <ng-content />
  `,
  styles: `
    :host { display: block; }
    /* Les variables CSS et les propriétés héritées (font, color) ENTRENT dans le shadow root */
    h3 { color: var(--atlas-accent, #2b45d4); font: inherit; font-weight: 650; }
  `,
})
export class AtlasCard {
  readonly title = input('');
}

Build, bundle, distribution

Pour utiliser les éléments en dehors d’une application Angular, il faut un build séparé : une application dont le main.ts appelle createApplication() et customElements.define(), sans composant racine. Le schematic Native Federation propose même l’option --webcomponent, qui génère exactement ce bootstrap (voir Micro frontends).

Le coût, c’est le runtime. Un seul élément simple, construit avec Angular 22.2.2 en production, zoneless, représente ≈ 110 kB minifiés, ≈ 33 kB transférés (estimation du CLI), soit pratiquement @angular/core + platform-browser + un peu de RxJS. Trois widgets issus de trois builds différents sur la même page, ce sont trois copies du runtime, trois injecteurs et trois applications qui ne partagent aucun service.

  • Regroupez les widgets d’une équipe dans un seul bundle : un createApplication(), plusieurs appels à define().
  • Des noms stables pour l’intégration via <script src> : --output-hashing=none et des dossiers versionnés (/1.4.0/main.js), pour pouvoir utiliser un cache long sans hash dans le nom.
  • Lazy à l’intérieur : @defer et import() fonctionnent aussi dans un élément, la partie lourde du widget peut donc arriver quand on en a besoin.
# un projet séparé dans le workspace, uniquement pour les widgets
ng generate application atlas-widgets --ssr=false
# main.ts : createApplication + customElements.define (voir main.ts ci-dessus)

# noms de fichiers stables, pour qu’un CMS puisse inclure <script src=".../main.js">
ng build atlas-widgets --output-hashing=none
# → publier dans des dossiers versionnés : /atlas-widgets/1.4.0/main.js
#   (cache long, pas de hash dans le nom ; les chunks lazy gardent leur hash)

# mesuré avec Angular 22.2.2, un élément simple, zoneless, build de production :
#   main.js ≈ 110 kB minifiés, ≈ 33 kB de transfert estimés par le CLI
# chaque bundle construit séparément apporte sa propre copie de
# @angular/core + @angular/platform-browser

Utilisation depuis d’autres environnements

Pour du HTML statique, un <script type="module"> et la balise suffisent. Chacun des autres environnements a un petit détail de configuration :

  • Angular : le composant hôte déclare schemas: [CUSTOM_ELEMENTS_SCHEMA]. Les Bindings [prop] et (event) fonctionnent, mais sans vérification des types ; si vous êtes dans la même application, utilisez directement le composant.
  • React 19 prend entièrement en charge les custom elements : les props qui correspondent à une propriété de l’élément sont définies comme propriétés DOM (dans les versions antérieures, elles arrivaient comme attributs). Pour les événements personnalisés, l’option portable est un ref + addEventListener.
  • Vue 3 doit savoir que la balise n’est pas un composant Vue : l’option de compilation isCustomElement.
  • CMS : l’éditeur n’insère que du HTML, donc tout doit être configurable via des attributs de type string. Concevez les inputs comme des primitives ; pour les configurations complexes, utilisez un attribut JSON avec un transform qui appelle JSON.parse.
// Utiliser un custom element (le vôtre ou celui d’une autre équipe) dans un composant Angular
import { CUSTOM_ELEMENTS_SCHEMA, Component, signal } from '@angular/core';

@Component({
  selector: 'app-product',
  // sans le schema : erreur « 'atlas-rating' is not a known element »
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <atlas-rating
      [value]="rating()"
      max-stars="5"
      (ratingChange)="rating.set($any($event).detail.value)"
    ></atlas-rating>
  `,
})
export class Product {
  protected readonly rating = signal(4);
}
// [value] définit la propriété DOM, (ratingChange) fait un addEventListener,
// mais sans aucune vérification des types.

Le contenu d’un custom element n’apparaît qu’après le téléchargement et l’exécution du script. Pour un texte essentiel au SEO ou au LCP (le titre de la page, le prix, le contenu principal), un widget côté client est un mauvais choix : voir SSR.

Communication entre applications

Quand plusieurs applications indépendantes cohabitent sur la même page (widgets d’équipes différentes, micro frontends), elles ne partagent pas d’injecteur. Il reste les mécanismes de la plateforme, dans l’ordre où il vaut la peine de les essayer :

MécanismePortéeCe que vous pouvez envoyerÀ surveiller
Attributs / propriétés + événements sur l’élémentl’hôte ↔ son élémentn’importe quoi (propriétés), detail (événements)c’est le contrat public du widget : versionnez-le
CustomEvent sur windowla même pagen’importe quel objet (une référence, pas une copie)noms préfixés, le « late subscriber » ; ce n’est pas de l’état
BroadcastChannelonglets, fenêtres, iframes, workers de même originevaleurs clonables (structured clone)n’atteint pas l’expéditeur ; close() à la fin
postMessageiframe / window, y compris d’une autre originevaleurs clonablesun targetOrigin explicite, vérifiez event.origin

Un bus sur window est le contrat le plus simple entre applications d’une même page : noms d’événements préfixés (atlas:cart:add), types dans un petit package versionné, deux fonctions emit/listen. Le piège classique : un événement n’est pas de l’état, et une application montée plus tard a raté tout ce qui s’est passé avant. La solution est un handshake : au démarrage, on demande l’état, et son propriétaire répond.

// contracts/atlas-events.ts — un petit package versionné, partagé par les équipes
export interface AtlasEvents {
  'atlas:cart:add': { sku: string; qty: number };
  'atlas:cart:changed': { count: number; total: number };
  'atlas:cart:state-request': { from: string };
}

export function emit<K extends keyof AtlasEvents>(type: K, detail: AtlasEvents[K]): void {
  window.dispatchEvent(new CustomEvent(type, { detail }));
}

export function listen<K extends keyof AtlasEvents>(
  type: K,
  fn: (detail: AtlasEvents[K]) => void,
): () => void {
  const handler = (e: Event) => fn((e as CustomEvent<AtlasEvents[K]>).detail);
  window.addEventListener(type, handler);
  return () => window.removeEventListener(type, handler);
}
LiveDeux applications sur la même page, un bus sur window
Application A · Équipe Catalogue dans le panier : ?
Laptop Pro 14
Studio Headphones
Ergo Mouse
Application B · Équipe Panier 0 produits · 0 RON
Le panier est vide.
Trafic sur window (un troisième listener indépendant)
  • Ajoutez un produit depuis A.
Ajoutez des produits depuis A : B possède le panier et annonce atlas:cart:changed. Décochez « A demande l’état au démarrage », puis démontez et remontez A : elle ne sait plus combien il y a de produits. Avec le handshake coché, B répond immédiatement. Démontez B : A continue d’émettre, sans aucune erreur.

BroadcastChannel relie tous les contextes de même origine qui ouvrent un canal du même nom : onglets, fenêtres, iframes, workers. Le message est copié par structured clone (pas de fonctions, pas d’instances de classe avec méthodes) et n’atteint pas l’objet qui l’a envoyé. Usages typiques : déconnexion dans tous les onglets, synchronisation du panier, invalidation d’un cache. Pour les iframes cross-origin, il reste postMessage, avec l’origine vérifiée aux deux extrémités.

LiveBroadcastChannel entre onglets
Cet onglet : #v4xkAutres onglets détectés : aucun pour l’instantOuvrir la page dans un nouvel onglet ↗
État partagé
0
dernière écriture : —
Message
L’expéditeur ne reçoit pas son propre message, seulement les autres onglets.
  • Ouvrez un second onglet et cliquez sur +1 dedans.
Ouvrez la page dans un second onglet (le bouton ci-dessus), placez les onglets côte à côte et cliquez sur +1 ou envoyez un message. Un nouvel onglet reçoit l’état courant via le handshake ; à sa fermeture, les autres reçoivent « bye ».
// Même origine : autres onglets, fenêtres, iframes, workers
@Injectable({ providedIn: 'root' })
export class SessionSync {
  private readonly channel = new BroadcastChannel('atlas-session');
  private readonly router = inject(Router);

  constructor() {
    this.channel.onmessage = (e: MessageEvent<{ type: 'logout' }>) => {
      if (e.data?.type === 'logout') this.router.navigateByUrl('/login');
    };
    inject(DestroyRef).onDestroy(() => this.channel.close());
  }

  logoutEverywhere(): void {
    // atteint les autres onglets, PAS cet objet
    this.channel.postMessage({ type: 'logout' });
  }
}

Quand OUI, quand NON

Angular Elements
  • Des widgets pour des pages que vous ne contrôlez pas : un CMS (WordPress, AEM, Sitecore), des sites de clients, un chat de support, un configurateur de produit.
  • Un design system utilisé depuis plusieurs frameworks, quand son équipe écrit en Angular.
  • Des îlots interactifs dans une application côté serveur (PHP, Java, .NET) qui ne sera pas réécrite de sitôt.
  • Des micro frontends qui ne peuvent pas partager la même version d’Angular : chaque élément apporte son runtime, les versions peuvent donc différer.
  • Du HTML dynamique (venant d’un CMS, de markdown) inséré avec innerHTML dans une application Angular, où des composants ordinaires ne démarreraient pas.
Autre chose
  • Dans la même application Angular → des composants ordinaires (ou des composants dynamiques avec createComponent/NgComponentOutlet).
  • Un contenu critique pour le SEO ou le LCP → rendu côté serveur (SSR, SSG), pas un widget côté client.
  • Plusieurs applications Angular de même version qui doivent partager le runtime → Native Federation, pas N bundles séparés.
  • Seulement pour réutiliser du code entre les applications Angular d’une même équipe → une bibliothèque dans un monorepo.
  • De minuscules widgets sur des pages sensibles au poids (un bouton « partager ») → 33 kB de runtime ne se justifient pas ; un web component écrit à la main ou Lit conviennent mieux.

Pièges fréquents

  • Le selector du composant utilisé comme balise → deux instances sur le même élément. Utilisez un selector distinct (atlas-rating-cmp).
  • define() appelé deux fois (HMR, tests, deux bundles qui définissent la même balise) → NotSupportedError. Vérifiez customElements.get() ; entre équipes, préfixez les balises avec le nom du produit.
  • Attendre que les événements remontent → un listener sur document qui ne reçoit rien. Écoutez sur l’élément, ou réémettez explicitement avec bubbles: true, composed: true.
  • Des outputs nommés change, input, click → ils se mélangent avec les événements natifs des éléments internes qui atteignent l’hôte. Utilisez des noms spécifiques (ratingChange).
  • Un input numérique sans transform → l’attribut arrive en string et maxStars() + 1 devient '71'. Mettez numberAttribute / booleanAttribute sur chaque input configurable depuis le HTML.
  • Des objets via des attributs → le composant reçoit "[object Object]". Envoyez les objets et tableaux comme propriétés.
  • Attendre de l’élément qu’il voie les services du composant parent → il utilise l’injecteur donné à createCustomElement, pas l’endroit du DOM où vous l’avez placé. Les services des providers d’un composant ou d’une route ne l’atteignent pas.
  • Des enfants ajoutés après la création → les nœuds pour <ng-content> ne sont collectés qu’une fois, à la connexion. Le contenu dynamique passe par des propriétés.
  • Réutiliser un élément détruit → HTML mort, NG0953, puis NotSupportedError. Créez un nouvel élément.
  • Des listeners et canaux non fermés → après chaque montage, un window.addEventListener de plus ou un BroadcastChannel ouvert de plus. Nettoyez dans DestroyRef.onDestroy.
  • postMessage(data, '*') et aucune vérification de event.origin → toute page qui vous charge dans une iframe peut lire ou envoyer des messages. Voir Sécurité.

Aide-mémoire

// 1. Le composant : des inputs avec transform pour les attributs, des outputs aux noms spécifiques
readonly maxStars = input(5, { transform: numberAttribute }); // <x max-stars="7">
readonly ratingChange = output<RatingChange>();               // CustomEvent, detail

// 2. Définition : une seule fois, balise ≠ selector, balise avec un tiret
createApplication({ providers: [] }).then(({ injector }) => { // bundle séparé
  // (dans une application existante : const injector = inject(ApplicationRef).injector)
  if (!customElements.get('atlas-rating')) {
    customElements.define('atlas-rating', createCustomElement(AtlasRating, { injector }));
  }
});

// 3. Utilisation
el.setAttribute('max-stars', '7');       // string → transform
el.maxStars = 7;                         // n’importe quel type
el.addEventListener('ratingChange', fn); // sur l’élément : bubbles false, composed false

// 4. Cycle de vie : append = création (synchrone) ; remove = destroy après 10 ms ;
//    ne rattachez pas une instance détruite → créez un autre élément

// 5. Styles : ViewEncapsulation.ShadowDom + variables CSS + ::part() pour le thème

// 6. Communication : attributs/propriétés/événements → CustomEvent sur window (typé)
//    → BroadcastChannel (onglets, même origine) → postMessage + origine (iframes)

Chap. 13 · 0 sur 4 terminées

Leçon terminée ?

Marquez-la : elle apparaîtra sur votre carte, dans le sommaire et sur la page d’accueil.

Se marque toute seule une fois la leçon lue en entier.

  1. Micro frontends
  2. Web Components avec Angular Elements
  3. Monorepo, bibliothèques et frontières
  4. Architecture hexagonale (ports et adapters)
Voir toute votre carte

Les 75 pages · tapez pour filtrer

  • 01Nouveautés d’Angular 21 & 22Tous les changements importants de la v21 et de la v22, en bref, avec un lien vers la page qui explique chacun.Départ
  • 01Votre première application, pas à pasUne liste de lecture construite d’un composant vide jusqu’à une application terminée — signals, @for, inputs et outputs, un service et un formulaire, pas à pas.Départ
  • 01Décisions rapides« Où mettre ça ? », « Signal ou Observable ? », « Shared ou feature ? » — des réponses courtes aux questions de tous les jours.Départ
  • 01Questions d’entretien Angular60 vraies questions d’entretien pour profils junior, confirmé et senior, avec la réponse à donner, la relance et les signaux d’alerte. Plus un entretien blanc.Départ
  • 02Standalone et bootstrapComment une application démarre sans NgModule : bootstrapApplication, app.config.ts, imports dans le composant.Fondamentaux modernes
  • 02Data binding et événementsInterpolation, bindings de propriétés et d’attributs, événements avec $event et filtres de touches, et comment [( )] est construit — ce qui entre dans le DOM et ce qui en revient.Fondamentaux modernes
  • 02Control Flow : @if, @for, @switchLa syntaxe de blocs qui remplace *ngIf/*ngFor/ngSwitch, avec track, @empty et vérifications d’exhaustivité.Fondamentaux modernes
  • 02@let et les nouvelles expressions de templateVariables locales avec @let, fonctions fléchées, spread, regex, instanceof, commentaires dans les balises.Fondamentaux modernes
  • 02@defer — Lazy Loading dans les templatesChargez les composants lourds uniquement quand c’est nécessaire : triggers, prefetch, placeholder, loading, error.Fondamentaux modernes
  • 02Bindings de classe et de stylePourquoi [class.x] et [style.x] ont remplacé ngClass et ngStyle (avec migration automatique depuis la v21).Fondamentaux modernes
  • 02Styles des composants et encapsulation:host, les trois modes d’encapsulation, les thèmes avec des variables CSS, et pourquoi ::ng-deep est un dernier recours.Fondamentaux modernes
  • 02Pipes : quand et commentPipes pures vs impures, pipes personnalisées, et comment choisir entre une pipe, computed() et une méthode.Fondamentaux modernes
  • 03signal() et computed()Les primitives réactives : état modifiable, état dérivé, égalité, untracked et règles d’immutabilité.Signals et réactivité
  • 03effect() — quand OUI, quand NONEffets de bord synchronisés avec les Signals : localStorage, logs, API impératives. Et leurs pièges.Signals et réactivité
  • 03linkedSignal() — état dérivé modifiableUn signal qui se réinitialise quand sa source change, mais que vous pouvez aussi modifier à la main.Signals et réactivité
  • 03resource, rxResource, httpResourceDes données asynchrones sous forme de Signals : status, value, error, reload, annulation automatique. Stable depuis la v22.Signals et réactivité
  • 03debounced() — signals différésLa version « debounce » d’un signal, sans RxJS. Parfait pour la recherche.Signals et réactivité
  • 03Listes : rechercher, filtrer, trier, paginerUne chaîne de signals computed qui ne recalcule que ce qui a changé, avec les filtres dans l’URL pour qu’un lien rouvre la même vue.Signals et réactivité
  • 03Les Observables depuis zéroCe qu’est un Observable, subscribe et unsubscribe, les opérateurs que vous utilisez chaque semaine, froid ou chaud — et quand un signal suffit.Signals et réactivité
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — et quand RxJS reste le bon choix.Signals et réactivité
  • 04input() et output()Les inputs comme Signals (required, transform, alias) et les outputs typés. Les données descendent, les événements remontent.Composants : communication
  • 04model() — binding bidirectionnelLe « banana-in-a-box » [( )] avec une seule API. Quand l’utiliser, et quand input + output est préférable.Composants : communication
  • 04viewChild et contentChildLes queries comme Signals : viewChild, viewChildren, contentChild, contentChildren, l’option read.Composants : communication
  • 04Lifecycle et DestroyRefQuels hooks comptent encore à l’ère des Signals, afterNextRender, DestroyRef et ngOnChanges typé.Composants : communication
  • 04Projection de contenung-content avec select, plusieurs slots, contenu par défaut et le piège de la projection conditionnelle.Composants : communication
  • 04ng-template et ngTemplateOutletLes templates comme valeurs : les render props en Angular, contexte typé et personnalisation des composants.Composants : communication
  • 05Smart vs DumbContainer vs présentation : qui détient l’état, qui se contente d’afficher, et comment bien les séparer.Conception des composants
  • 05Composants génériques <T>Un DataTable<T> ou un Select<T> correctement typé, avec des templates pour les cellules.Conception des composants
  • 05Composants dynamiquesNgComponentOutlet, createComponent avec inputBinding/outputBinding, et un tableau de bord construit à partir d’une configuration.Conception des composants
  • 05Directives et composition d’hôteLes directives d’attribut, les métadonnées host: {} et la Directive Composition API (hostDirectives).Conception des composants
  • 05Contrôles de formulaire personnalisésFormValueControl (Signal Forms) vs ControlValueAccessor : une notation par étoiles, dans les deux variantes.Conception des composants
  • 05Change DetectionOnPush est la valeur par défaut depuis la v22, Zone.js a disparu depuis la v21. Ce qui déclenche le rendu et comment éviter les mauvaises surprises.Conception des composants
  • 06inject() et @Service()Le nouveau décorateur @Service() de la v22, inject() vs le constructeur, et le contexte d’injection.Dependency Injection
  • 06Providers et hiérarchie des injecteursRoot vs route vs composant, useClass/useValue/useFactory/useExisting, et les modificateurs optional/self/skipSelf.Dependency Injection
  • 06InjectionToken et provideX()Configuration typée, fonctions provideFeature(), makeEnvironmentProviders et initializers.Dependency Injection
  • 06injectAsync() — services en Lazy LoadingChargez un service lourd uniquement quand vous en avez besoin, avec prefetch à l’idle.Dependency Injection
  • 07Structure des dossierscore / shared / features, ce que contient chacun, et à quoi ressemble l’intérieur d’une feature.Architecture d’application
  • 07Shared vs featureQuand déplacer un composant dans shared et (surtout) quand NE PAS le faire. Une checklist concrète.Architecture d’application
  • 07Interfaces et modèlesInterface en ligne ou fichier séparé ? DTO vs modèle de domaine, type vs interface, où les ranger.Architecture d’application
  • 07State Management avec les SignalsStores simples à base de Signals, facades, état local vs global, et quand NgRx devient nécessaire.Architecture d’application
  • 07Guide de style et nommageLes conventions officielles actuelles : noms de fichiers sans suffixes, protected, readonly, ordre des membres.Architecture d’application
  • 07Barrels, alias et frontièresindex.ts, alias de chemins, API publique d’une feature et règles de dépendances vérifiables automatiquement.Architecture d’application
  • 07Erreurs : @boundary et ErrorHandlerLe nouveau bloc @boundary de la v22.2, l’ErrorHandler global, les erreurs HTTP et les erreurs de resource.Architecture d’application
  • 08Routing moderne et Lazy LoadingloadComponent, routes enfants, paramètres comme inputs et héritage des paramètres (le nouveau comportement par défaut de la v22).Routing
  • 08Guards et resolverscanActivate, canMatch, canDeactivate et resolve sous forme de fonctions, avec redirections via UrlTree / RedirectCommand.Routing
  • 09Signal Forms — les basesDes formulaires bâtis sur les Signals : form(), [formField], validation par schéma. Stable depuis la v22.Formulaires
  • 09Signal Forms — avancéValidation personnalisée et inter-champs, async/HTTP avec debounce, listes dynamiques, désactivé/masqué, schémas réutilisables.Formulaires
  • 09Reactive Forms typésFormGroup typé, nonNullable, FormArray — et leur cohabitation avec les Signal Forms.Formulaires
  • 10HttpClient, interceptors et couche d’APIHttpClient sans configuration (v21), Fetch par défaut (v22), interceptors fonctionnels et une couche d’API avec mapping des DTO.HTTP et données
  • 11Performance — une checklistCe qui compte vraiment : OnPush + Signals, track, @defer, routes en Lazy Loading, images, bundles et Hydration.Qualité et performance
  • 11Tests avec VitestVitest est le runner par défaut depuis la v21. TestBed, composants avec Signals, services, HTTP et harnesses.Qualité et performance
  • 11DevTools : regarder dans votre applicationChrome DevTools et Angular DevTools pas à pas : ce que montre chaque panneau, avec des exercices qui fonctionnent sur ce site.Qualité et performance
  • 11Angular AriaDes composants headless et accessibles que vous stylez comme vous voulez : tabs, accordion, menu, listbox...Qualité et performance
  • 12SSR, Prerendering et modes de renduQuand rendre sur le serveur, quand au build et quand uniquement dans le navigateur : RenderMode par route, app.routes.server.ts et les coûts réels.SSR et Hydration
  • 12Hydration : event replay et incrémentaleComment le navigateur reprend le HTML du serveur sans le re-rendre : Hydration complète, event replay, @defer (hydrate on …) et erreurs de mismatch.SSR et Hydration
  • 12Code compatible serveurwindow n’existe pas sur le serveur : PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, le cache HTTP et PendingTasks.SSR et Hydration
  • 13Micro frontendsQuand découper une application en plusieurs déploiements vaut le coup, et quand c’est juste de la complexité : Native Federation, Module Federation, iframes, Web Components.Architecture à grande échelle
  • 13Web Components avec Angular ElementscreateCustomElement : un composant Angular utilisé comme balise HTML n’importe où, plus la communication entre applications via des événements et BroadcastChannel.Architecture à grande échelle
  • 13Monorepo, bibliothèques et frontièresUn workspace avec plusieurs applications et bibliothèques : ng generate library, Nx, points d’entrée secondaires, publication et règles de dépendances.Architecture à grande échelle
  • 13Architecture hexagonale (ports et adapters)Un domaine sans Angular, des ports sous forme de classes abstraites et des adapters interchangeables : HTTP, localStorage ou en mémoire, sans toucher à l’UI.Architecture à grande échelle
  • 14Les Signals sous le capotLe graphe réactif : producteurs et consommateurs, push-pull, pourquoi computed est lazy et sans glitch, le problème du diamant et quand les effects s’exécutent.Réactivité avancée
  • 14RxJS avancé : opérateurs d’ordre supérieurswitchMap, mergeMap, concatMap et exhaustMap sur un diagramme interactif, plus retry avec backoff, shareReplay et comment éviter les fuites.Réactivité avancée
  • 14NgRx SignalStoreState Management à grande échelle sur les Signals : withState, withComputed, withMethods, rxMethod, entités et hooks — et quand un store simple suffit.Réactivité avancée
  • 14Machines à états avec les signalsFini les états impossibles : une machine typée sur des signals, avec transitions, gardes et un diagramme d’états en direct.Réactivité avancée
  • 15WebSockets : un temps réel qui résiste au réseauUn socket typé sous forme de signals : reconnexion avec backoff et jitter, heartbeat, file d’attente hors ligne et sécurité SSR.Temps réel et hors ligne
  • 15Mises à jour optimistes et rollbackUne UI instantanée qui reste honnête : snapshot et rollback, réponses dans le désordre, clés d’idempotence.Temps réel et hors ligne
  • 15Offline-first : Service Worker et cacheLe Service Worker d’Angular : stratégies de cache, annonce des nouvelles versions et file d’attente pour les modifications faites hors ligne.Temps réel et hors ligne
  • 16Web WorkersDes calculs lourds sans bloquer l’UI : un vrai worker dans la page, l’échange de messages et quand ça n’en vaut pas la peine.UI avancée et performance
  • 16Listes géantes : virtual scroll100 000 lignes sans latence : le virtual scroll du CDK, itemSize, trackBy et les alternatives (pagination, scroll infini).UI avancée et performance
  • 16Animations modernes et View Transitionsanimate.enter et animate.leave à la place de @angular/animations (déprécié), transitions CSS et View Transitions à la navigation.UI avancée et performance
  • 16Fuites mémoire : les trouver et les corrigerAbonnements, timers et listeners qui survivent à leur composant — comment les repérer dans les DevTools et les fermer.UI avancée et performance
  • 17Sécurité : XSS, sanitisation et CSPCe qu’Angular fait automatiquement contre le XSS, quand bypassSecurityTrust devient dangereux, plus la CSP avec nonce, Trusted Types et XSRF.Sécurité et internationalisation
  • 17Authentification : tokens et refreshUn interceptor qui ajoute le token, le renouvelle une seule fois sur une 401 et rejoue les requêtes en attente — plus où conserver le token.Sécurité et internationalisation
  • 17OAuth 2.1 et OpenID Connect avec PKCELe flux authorization code avec PKCE, pas à pas : verifier et challenge, state et nonce, l’ID token, et pourquoi un BFF est plus sûr.Sécurité et internationalisation
  • 17Internationalisation (i18n)$localize, l’attribut i18n, ICU plural/select, un build par langue vs des traductions à l’exécution, et le formatage selon la locale.Sécurité et internationalisation