AngularAtlasAusg. 22.2

Kap. 13Architektur im großen Maßstab58 / 75

Web Components mit Angular Elements

createCustomElement: eine Angular-Komponente, die überall als HTML-Tag nutzbar ist, plus Kommunikation zwischen Apps über Events und BroadcastChannel.

stabil ~0 Min. Lesezeit Offizielle Doku

Ein Custom Element (Web Component) ist ein HTML-Tag, den du selbst definierst — <atlas-rating> — und den der Browser überall erkennt: in einer statischen Seite, in einem CMS, in einer React-Anwendung oder in einer anderen Angular-Anwendung. @angular/elements ist die Brücke: createCustomElement() macht aus einer Angular-Komponente eine Klasse, die HTMLElement erweitert. Inputs werden zu Attributen und Properties, Outputs werden zu CustomEvents, und die Komponente erzeugt sich selbst, sobald der Tag ins DOM kommt.

Die Beispiele auf dieser Seite sind echt: Wir definieren die Elemente zur Laufzeit mit customElements.define, erzeugen sie mit document.createElement in einem Container, den Angular nicht verwaltet, und hören sie mit addEventListener ab — genau so, wie es eine Seite täte, die nichts von Angular weiß. Alles läuft in dieser Anwendung, die zoneless ist.

Kurz gesagt
  • createCustomElement macht aus einer Komponente eine Web Component, die du auf jeder Seite nutzen kannst: React, Vue, ein CMS oder reines HTML.
  • Inputs werden zu Properties und Attributen (in kebab-case), Outputs zu CustomEvent-Events.
  • Mit Shadow DOM vermischen sich die Styles des Elements und der Seite nicht mehr.
  • Jedes Element bringt die Angular-Runtime mit. Mehrere Elemente auf derselben Seite sollten aus demselben Bundle kommen.
  • Es lohnt sich für Widgets in Seiten, die du nicht kontrollierst, nicht für eine normale Angular-App.
Warum das wichtig ist

Angular Elements ist das richtige Werkzeug, wenn eine Komponente außerhalb einer Angular-Anwendung leben muss: Widgets für ein CMS oder für die Websites von Kunden, ein Design System, das Teams mit unterschiedlichen Frameworks nutzen, Micro Frontends, die nicht dieselbe Angular-Version teilen können. Innerhalb einer gewöhnlichen Angular-Anwendung ist es fast immer die falsche Wahl: Du verlierst die Typprüfung im Template, die DI-Hierarchie und die dynamische Content-Projection.

Was createCustomElement macht

createCustomElement(Component, { injector }) zeigt nichts an. Es liefert eine Klasse (NgElementConstructor) zurück, die du mit customElements.define(tag, klasse) registrierst. Die Klasse liest die Metadaten der Komponente (reflectComponentType) und bereitet drei Dinge vor:

  • observedAttributes — die Namen der Inputs in kebab-case (maxStars → max-stars), damit der Browser sie über jede Attributänderung benachrichtigt;
  • einen Getter und einen Setter auf dem Prototype für jeden Input, sodass el.maxStars = 7 wie eine gewöhnliche DOM-Property funktioniert;
  • eine Strategie (NgElementStrategy), die bei connectedCallback createComponent mit dem Element als hostElement aufruft, die View an ApplicationRef hängt und ein erstes detectChanges() ausführt — synchron, bevor append() zurückkehrt.

Der Injector, den du übergibst, ist die Quelle für die Services der Komponente (HttpClient, Stores, Konfigurations-Tokens). In einem Bundle nur für Widgets bekommst du ihn von createApplication(), einer Anwendung ohne Root-Komponente; in einer bestehenden Anwendung aus 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',               // ≠ Tag-Name (siehe unten)
  encapsulation: ViewEncapsulation.ShadowDom, // das CSS der Host-Seite kommt nicht hinein
  template: `
    @for (s of stars(); track s) {
      <button type="button" [class.on]="s <= current()" [disabled]="readonly()"
              (click)="pick(s)">★</button>
    }
  `,
})
export class AtlasRating {
  // Attribute: value, max-stars, readonly
  readonly value = input(0, { transform: numberAttribute });
  readonly maxStars = input(5, { transform: numberAttribute });
  readonly readonly = input(false, { transform: booleanAttribute });

  // wird zu einem CustomEvent 'ratingChange', der Wert steckt in event.detail
  readonly ratingChange = output<RatingChange>();

  // lokaler Zustand, wird zurückgesetzt, wenn der Host einen anderen Wert sendet
  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 });
  }
}
Der Tag ist nicht der Selektor

Der offizielle Leitfaden verlangt ausdrücklich, den Selektor der Komponente nicht als Tag-Namen zu verwenden. Wenn atlas-rating zugleich Selektor und Custom Element ist und die Komponente außerdem in ein Template importiert wird, erzeugt Angular zwei Instanzen auf demselben Element. Daher die Konvention atlas-rating-cmp (Selektor) vs. atlas-rating (Tag). Der Tag muss einen Bindestrich enthalten, und define() auf einem bereits registrierten Namen wirft NotSupportedError — deshalb prüfst du zuerst mit customElements.get().

Zone.js wird nicht gebraucht. Ab v19 nutzen die Elemente den gewöhnlichen Change-Detection-Scheduler (Changelog 19.0.0: „switch to ComponentRef.setInput & remove custom scheduler“), und ab v21 sind Anwendungen standardmäßig zoneless. Ein setAttribute landet bei componentRef.setInput(), das die View markiert und ein Rendering einplant; ein Klick im Template des Elements ändert ein Signal — genau wie in einer gewöhnlichen OnPush-Komponente.

Definieren in einer bestehenden Anwendung

Der typische Fall: Die Anwendung erhält HTML aus einem CMS und fügt es mit innerHTML ein. Die Angular-Komponenten in diesem HTML würden nie starten, aber Custom Elements starten von selbst, weil der Browser ihren Tag erkennt.

// In einer bestehenden Anwendung (z. B. HTML aus einem CMS, per innerHTML eingefügt)
import { ApplicationRef, inject, provideAppInitializer } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { AtlasRating } from './rating';

function defineAtlasElements(): void {
  const injector = inject(ApplicationRef).injector; // Root: lebt so lange wie die Anwendung
  // define() auf einem bereits registrierten Namen wirft NotSupportedError
  if (!customElements.get('atlas-rating')) {
    customElements.define('atlas-rating', createCustomElement(AtlasRating, { injector }));
  }
}

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

Attribute, Properties, Events

Ein Element hat zwei „Eingänge“ für denselben Input. Das Attribut (max-stars="7") ist immer ein String — deshalb brauchen numerische oder boolesche Inputs transform: numberAttribute / booleanAttribute; ohne Transform wird aus maxStars() + 1 der Wert '71'. Die Property (el.maxStars = 7) akzeptiert jeden Typ, Objekte und Arrays gehen also nur hierüber. Beim Lesen liefert el.maxStars den aktuellen Wert des Inputs; ab v21 gilt das auch für input() auf Signals (früher musste man el.maxStars() aufrufen).

Input / OutputHTML-AttributJS-PropertyGut zu wissen
maxStars
transform: numberAttribute
max-stars="7"el.maxStars = 7das Attribut kommt als '7' an, der Transform macht eine Zahl daraus
readonly
transform: booleanAttribute
readonlyel.readonly = trueremoveAttribute sendet null → false
userId
Alias: 'user'
user="42"el.userIddas Attribut nutzt den Alias, die Property den Namen aus der Klasse
items
input<Item[]>
—el.items = [...]über ein Attribut käme "[object Object]" an
ratingChange
output<T>()
—addEventListener('ratingChange', …)CustomEvent, der Wert steckt in event.detail
export class Widget {
  // das Attribut = der Name des Inputs (oder sein Alias) in 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[]>([]);                               // nur als Property

  // ein Output wird zu einem CustomEvent mit dem Namen des Outputs (oder des Alias)
  readonly ratingChange = output<RatingChange>();           // 'ratingChange'
  readonly closed = output<void>({ alias: 'atlasClosed' }); // 'atlasClosed'
}

// Was createCustomElement für die Klasse oben erzeugt:
//   static observedAttributes = ['max-stars', 'compact', 'user', 'items']
//   Getter/Setter auf dem Prototype:  maxStars, compact, userId, items
//   (die Property nutzt den Namen aus der Klasse, das Attribut den Alias)

Das Event wird mit new CustomEvent(name, { detail }) erzeugt — ohne bubbles und ohne composed. Ein Listener auf document oder auf dem übergeordneten Container bekommt nichts mit; du hörst direkt am Element zu. Im Beispiel unten hat der Container ebenfalls einen Listener für ratingChange: Er wird nie im Log auftauchen.

LiveEchtes Element: <atlas-rating> zur Laufzeit definiert
Host-Seite (einfacher DOM-Code)
Senden als
// drücke einen Button oder ändere ein Feld
Container, den Angular nicht verwaltet
im DOM: neinerzeugte Komponenten: 0zerstört: 0gelesen el.value: —
Was passiert (addEventListener + Callbacks)
  • 09:11:30 customElements.define('atlas-rating', …) — jetzt definiert
Ändere die Felder im Modus „Attribut“, dann im Modus „Property“, und beobachte den gesendeten Befehl und was el.value liest. Klicke auf die Sterne: Das Log zeigt event.detail und bubbles=false. „Verschieben“ erhält den Zustand; el.remove() gefolgt von „append(gleiche Instanz)“ zeigt die unten beschriebene Falle.

Beachte ein Detail: Nachdem du auf einen Stern geklickt hast, bleibt el.value der Wert, den das Element vom Host erhalten hat. Der Input ist ein Einwegkanal; die Komponente hält die Auswahl in einem linkedSignal und meldet sie per Event. Soll der Host die Quelle der Wahrheit sein, hört er das Event ab und schreibt el.value zurück — das Pattern der „Controlled Component“.

// 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); // ✗ empfängt nichts

// Wenn der Host wirklich Delegation braucht (ein Listener für 50 Widgets),
// sende explizit aus der Komponente ein Event, das bubbelt und die Shadow Root durchquert:
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 }),
    );
  }
}

Lifecycle

Die Komponente entsteht bei connectedCallback und stirbt bei disconnectedCallback, aber nicht sofort: Die Strategie plant destroy() nach 10 ms ein (DESTROY_DELAY = 10 im Quellcode des Pakets). Kehrt das Element vorher ins DOM zurück — ein Verschieben per remove() + append() im selben Task, etwa beim Sortieren einer Liste —, wird das Zerstören abgebrochen, und der interne Zustand bleibt erhalten. Inputs, die vor dem Verbinden gesetzt werden, werden gemerkt und beim Erzeugen angewendet. Außerdem werden Properties, die auf einem noch nicht definierten Element gesetzt wurden (das Skript des Widgets ist noch nicht geladen), beim Upgrade übernommen, sodass der Host el.items jederzeit setzen kann.

document.createElement('atlas-rating')
    constructor; noch keine Komponente (jetzt gesetzte Inputs werden gemerkt)
container.append(el)
    connectedCallback → createComponent(...) SYNCHRON, attachView(appRef), detectChanges()
el.setAttribute('value', '4')
    attributeChangedCallback → componentRef.setInput('value', '4') → transform → CD
el.value = 4
    setter on the prototype → derselbe Weg, ohne Umwandlung in einen String
el.remove()
    disconnectedCallback → destroy GEPLANT in 10 ms
    ↳ in < 10 ms wieder eingefügt (Move)  destroy abgebrochen: gleiche Instanz, gleicher Zustand
    ↳ nach 10 ms                         componentRef.destroy() → DestroyRef.onDestroy
container.append(el)   // gleiche Instanz, nach destroy
    ⚠︎ die Komponente wird NICHT neu erzeugt: totes HTML + NG0953 in der Konsole
document.createElement('atlas-rating')
    ✓ neue, saubere Instanz
Ein bereits zerstörtes Element erneut anhängen

Der offizielle Leitfaden warnt (Abschnitt Limitations, Issue #38778), dass das Abhängen und erneute Anhängen eines mit @angular/elements erzeugten Elements problematisch ist. In v22.2.2 lässt sich das Verhalten in der Demo beobachten: Sobald die Komponente zerstört wurde, erzeugt ein erneutes append derselben Instanz sie nicht neu. Das in der Shadow Root verbliebene HTML ist tot (Klicks bewirken nichts), el.value ist undefined, und Angular meldet NG0953 (Abonnieren eines zerstörten OutputRef). Ein zweiter Versuch wirft NotSupportedError aus attachShadow, weil ein Element nicht zwei Shadow Roots haben kann.

Das passiert, wenn jemand den DOM-Knoten wiederverwendet: das ng-if von AngularJS, ein CMS-Editor, der Blöcke verschiebt, ein Element-Cache in deinem Code. Die Lösung: Verwende die Instanz nicht wieder — erzeuge mit document.createElement ein anderes Element — oder lass es, wenn du es nur verstecken willst, mit dem Attribut hidden im DOM.

Shadow DOM und Styles

Mit Emulated (Standard) schreibt Angular die Selektoren der Komponente mit eindeutigen Attributen um: Deine Styles lecken nicht nach außen, aber das CSS der Seite kommt hinein. Ein Widget auf einer fremden Website erbt jede Regel wie .content p oder button { text-transform: uppercase !important } — der häufigste Bug bei Widgets. Mit ViewEncapsulation.ShadowDom wird das Template in eine Shadow Root gerendert (Modus open), und die Selektoren der Seite sehen nicht mehr hinein. Nur vererbte Properties (font, color) und CSS-Variablen kommen durch — genau das, was du für ein kontrolliertes Theme brauchst.

Eine wenig bekannte Nuance: Im Modus ShadowDom kopiert Angular in jede Shadow Root auch die Styles der anderen Komponenten derselben Anwendung. Bei Emulated-Komponenten ist das toter Ballast (ihre Selektoren passen nicht), aber eine Komponente mit ViewEncapsulation.None stylt am Ende auch das Innere des Widgets. ExperimentalIsolatedShadowDom (experimentell seit v21) legt nur die eigenen Styles der Komponente in die Shadow Root. Beim SSR werden beide Shadow-Modi als Emulated gerendert.

EncapsulationKommt das CSS der Seite hinein?None-Styles aus der AnwendungCSS-Variablen
Emulated (Standard)jajaja
ShadowDomneinja (in die Shadow Root kopiert)ja
ExperimentalIsolatedShadowDomexperimentellneinneinja
LiveDasselbe Widget, drei Encapsulations
Elementel.shadowRoot<style> in der Shadow Root
Hake die Kästchen nacheinander an. Das „CMS“-CSS zerstört nur die Emulated-Variante. Die None-Komponente erreicht auch ShadowDom (die Anzahl der style-Elemente steigt um eins), aber nicht die isolierte Variante. Die CSS-Variable themt alle drei.

Shadow DOM ändert mehr als nur Styles: Events, die eine Shadow Root verlassen, werden retargeted (event.target wird zum Host), document.querySelector findet von innen nichts, und E2E-Tests müssen die Shadow Root durchdringen. Für das Theming stellst du CSS-Variablen bereit und, wo mehr Kontrolle nötig ist, part-Attribute, die sich mit ::part() stylen lassen.

@Component({
  selector: 'atlas-card-cmp',
  encapsulation: ViewEncapsulation.ShadowDom,
  template: `
    <h3 part="title">{{ title() }}</h3>
    <ng-content />
  `,
  styles: `
    :host { display: block; }
    /* CSS-Variablen und vererbte Properties (font, color) GELANGEN in die Shadow Root */
    h3 { color: var(--atlas-accent, #2b45d4); font: inherit; font-weight: 650; }
  `,
})
export class AtlasCard {
  readonly title = input('');
}

Build, Bundle, Distribution

Um die Elemente außerhalb einer Angular-Anwendung zu nutzen, brauchst du einen separaten Build: eine Anwendung, deren main.ts createApplication() und customElements.define() aufruft, ohne Root-Komponente. Das Native-Federation-Schematic hat sogar die Option --webcomponent, die genau diesen Bootstrap erzeugt (siehe Micro Frontends).

Der Preis ist die Runtime. Ein einzelnes einfaches Element, mit Angular 22.2.2 im Production-Build, zoneless, kommt auf ≈ 110 kB minifiziert, ≈ 33 kB übertragen (die Schätzung der CLI) — praktisch @angular/core + platform-browser + ein bisschen RxJS. Drei Widgets aus drei verschiedenen Builds auf derselben Seite bedeuten drei Kopien der Runtime, drei Injectors und drei Anwendungen, die keinen Service teilen.

  • Fasse die Widgets eines Teams in einem einzigen Bundle zusammen: ein createApplication(), mehrere define()-Aufrufe.
  • Stabile Namen für die Einbindung per <script src>: --output-hashing=none und versionierte Ordner (/1.4.0/main.js), damit du einen langen Cache ohne Hash im Namen nutzen kannst.
  • Lazy im Inneren: @defer und import() funktionieren auch innerhalb eines Elements, sodass der schwere Teil des Widgets erst bei Bedarf kommen kann.
# ein eigenes Projekt im Workspace, nur für Widgets
ng generate application atlas-widgets --ssr=false
# main.ts: createApplication + customElements.define (siehe main.ts oben)

# stabile Dateinamen, damit ein CMS <script src=".../main.js">
ng build atlas-widgets --output-hashing=none
# → in versionierte Ordner veröffentlichen: /atlas-widgets/1.4.0/main.js
#   (langer Cache, kein Hash im Namen; die Lazy-Chunks behalten ihren Hash)

# gemessen mit Angular 22.2.2, ein einfaches Element, zoneless, Production-Build:
#   main.js ≈ 110 kB minifiziert, ≈ 33 kB Transfer laut CLI-Schätzung
# jedes separat gebaute Bundle bringt seine eigene Kopie von
# @angular/core + @angular/platform-browser

Verwendung aus anderen Umgebungen

Für statisches HTML genügen ein <script type="module"> und der Tag. Jede der anderen Umgebungen hat ein kleines Konfigurationsdetail:

  • Angular: Die Host-Komponente deklariert schemas: [CUSTOM_ELEMENTS_SCHEMA]. Die Bindings [prop] und (event) funktionieren, aber ohne Typprüfung — bist du in derselben Anwendung, nutze die Komponente direkt.
  • React 19 unterstützt Custom Elements vollständig: Props, die zu einer Property des Elements passen, werden als DOM-Properties gesetzt (in früheren Versionen kamen sie als Attribute an). Für Custom Events ist die portable Variante ein ref + addEventListener.
  • Vue 3 muss wissen, dass der Tag keine Vue-Komponente ist: die Compile-Option isCustomElement.
  • CMSs: Der Editor fügt nur HTML ein, also muss sich alles über String-Attribute konfigurieren lassen. Entwirf die Inputs als Primitive; für komplexe Konfigurationen nimmst du ein JSON-Attribut mit einem Transform, der JSON.parse aufruft.
// Ein Custom Element (dein eigenes oder das eines anderen Teams) in einer Angular-Komponente verwenden
import { CUSTOM_ELEMENTS_SCHEMA, Component, signal } from '@angular/core';

@Component({
  selector: 'app-product',
  // ohne das Schema: Fehler „'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] setzt die DOM-Property, (ratingChange) macht addEventListener —
// aber ganz ohne Typprüfung.

Der Inhalt eines Custom Elements erscheint erst, nachdem das Skript heruntergeladen und ausgeführt wurde. Für Text, der für SEO oder LCP wesentlich ist (der Seitentitel, der Preis, der Hauptinhalt), ist ein Client-seitiges Widget die falsche Wahl — siehe SSR.

Kommunikation zwischen Anwendungen

Wenn mehrere unabhängige Anwendungen auf derselben Seite leben (Widgets verschiedener Teams, Micro Frontends), teilen sie keinen Injector. Es bleiben die Mechanismen der Plattform, in der Reihenfolge, in der es sich lohnt, sie auszuprobieren:

MechanismusReichweiteWas du senden kannstWorauf du achten musst
Attribute / Properties + Events am Elementder Host ↔ sein Elementbeliebig (Properties), detail (Events)es ist der öffentliche Vertrag des Widgets: versioniere ihn
CustomEvent auf windowdieselbe Seitejedes Objekt (eine Referenz, keine Kopie)Namen mit Präfix, der „Late Subscriber“, es ist kein Zustand
BroadcastChannelTabs, Fenster, iframes, Worker — gleiche Originklonbare Werte (Structured Clone)erreicht den Absender nicht; am Ende close()
postMessageiframe / Fenster, auch andere Originklonbare Werteein explizites targetOrigin, event.origin prüfen

Ein Bus auf window ist der einfachste Vertrag zwischen Anwendungen auf derselben Seite: Event-Namen mit Präfix (atlas:cart:add), Typen in einem kleinen, versionierten Paket, zwei Funktionen emit/listen. Die klassische Falle: Ein Event ist kein Zustand — eine später gemountete Anwendung hat alles verpasst, was vorher passiert ist. Die Lösung ist ein Handshake — beim Start fragst du den Zustand ab, und sein Besitzer antwortet.

// contracts/atlas-events.ts — ein kleines, versioniertes Paket, das sich die Teams teilen
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);
}
LiveZwei Anwendungen auf derselben Seite, ein Bus auf window
Anwendung A · Team Katalog im Warenkorb: ?
Laptop Pro 14
Studio-Kopfhörer
Ergo-Maus
Anwendung B · Team Warenkorb 0 Produkte · 0 RON
Der Warenkorb ist leer.
Traffic auf window (ein dritter, unabhängiger Listener)
  • Füge ein Produkt aus A hinzu.
Füge Produkte aus A hinzu: B besitzt den Warenkorb und meldet atlas:cart:changed. Entferne den Haken bei „A fragt beim Start den Zustand ab“, hänge dann A aus und wieder ein: Es weiß nicht mehr, wie viele Produkte es gibt. Mit gesetztem Handshake antwortet B sofort. Hänge B aus: A sendet weiter, ohne jeden Fehler.

BroadcastChannel verbindet alle Kontexte derselben Origin, die einen Kanal mit demselben Namen öffnen: Tabs, Fenster, iframes, Worker. Die Nachricht wird per Structured Clone kopiert (keine Funktionen, keine Klasseninstanzen mit Methoden) und erreicht das Objekt nicht, das sie gesendet hat. Typische Einsätze: Logout in allen Tabs, Synchronisieren des Warenkorbs, Invalidieren eines Caches. Für iframes anderer Origin bleibt postMessage, mit an beiden Enden geprüfter Origin.

LiveBroadcastChannel zwischen Tabs
Dieser Tab: #6hobGesehene andere Tabs: derzeit keineSeite in neuem Tab öffnen ↗
Geteilter Zustand
0
letzter Schreibzugriff: —
Nachricht
Der Absender erhält seine eigene Nachricht nicht — nur die anderen Tabs.
  • Öffne einen zweiten Tab und drücke dort +1.
Öffne die Seite in einem zweiten Tab (der Button oben), lege die Tabs nebeneinander und drücke +1 oder sende eine Nachricht. Ein neuer Tab erhält den aktuellen Zustand per Handshake; wenn er sich schließt, empfangen die anderen „bye“.
// Gleiche Origin: andere Tabs, Fenster, iframes, Worker
@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 {
    // erreicht die anderen Tabs, NICHT dieses Objekt
    this.channel.postMessage({ type: 'logout' });
  }
}

Wann ja, wann nein

Angular Elements
  • Widgets für Seiten, die du nicht kontrollierst: ein CMS (WordPress, AEM, Sitecore), Websites von Kunden, ein Support-Chat, ein Produktkonfigurator.
  • Ein Design System, das aus mehreren Frameworks genutzt wird, wenn sein Team Angular schreibt.
  • Interaktive Inseln in einer serverseitigen Anwendung (PHP, Java, .NET), die in absehbarer Zeit nicht neu geschrieben wird.
  • Micro Frontends, die nicht dieselbe Angular-Version teilen können: Jedes Element bringt seine Runtime mit, die Versionen dürfen also abweichen.
  • Dynamisches HTML (aus einem CMS, aus Markdown), das per innerHTML in eine Angular-Anwendung eingefügt wird, wo gewöhnliche Komponenten nicht starten würden.
Etwas anderes
  • Innerhalb derselben Angular-Anwendung → gewöhnliche Komponenten (oder dynamische Komponenten mit createComponent/NgComponentOutlet).
  • Für SEO oder LCP kritischer Inhalt → Server-Side Rendering (SSR, SSG), kein Client-seitiges Widget.
  • Mehrere Angular-Anwendungen in derselben Version, die die Runtime teilen müssen → Native Federation, nicht N separate Bundles.
  • Nur um Code zwischen den Angular-Anwendungen desselben Teams wiederzuverwenden → eine Library in einem Monorepo.
  • Winzige Widgets auf gewichtssensiblen Seiten (ein „Teilen“-Button) → 33 kB Runtime sind nicht gerechtfertigt; eine handgeschriebene Web Component oder Lit passt besser.

Häufige Stolperfallen

  • Der Selektor der Komponente als Tag verwendet → zwei Instanzen auf demselben Element. Nimm einen separaten Selektor (atlas-rating-cmp).
  • define() zweimal aufgerufen (HMR, Tests, zwei Bundles, die denselben Tag definieren) → NotSupportedError. Prüfe customElements.get(); zwischen Teams versiehst du die Tags mit dem Produktnamen als Präfix.
  • Erwarten, dass Events nach oben bubbeln → ein Listener auf document, der nichts empfängt. Höre am Element zu oder sende explizit neu mit bubbles: true, composed: true.
  • Outputs mit den Namen change, input, click → sie vermischen sich mit den nativen Events der inneren Elemente, die den Host erreichen. Nimm spezifische Namen (ratingChange).
  • Ein numerischer Input ohne Transform → das Attribut kommt als String an, und aus maxStars() + 1 wird '71'. Setze numberAttribute / booleanAttribute auf jeden Input, der sich aus HTML konfigurieren lässt.
  • Objekte über Attribute → die Komponente erhält "[object Object]". Sende Objekte und Arrays als Properties.
  • Erwarten, dass das Element die Services der übergeordneten Komponente sieht → es nutzt den Injector, der an createCustomElement übergeben wurde, nicht die Stelle im DOM, an die du es gesetzt hast. Services aus den providers einer Komponente oder einer Route erreichen es nicht.
  • Nach dem Erzeugen hinzugefügte Kinder → die Knoten für <ng-content> werden nur einmal beim Verbinden eingesammelt. Dynamischer Inhalt wird über Properties gesendet.
  • Ein zerstörtes Element wiederverwenden → totes HTML, NG0953, dann NotSupportedError. Erzeuge ein neues Element.
  • Nicht geschlossene Listener und Kanäle → nach jedem Mounten ein weiterer window.addEventListener oder ein weiterer offener BroadcastChannel. Räume in DestroyRef.onDestroy auf.
  • postMessage(data, '*') und keine Prüfung von event.origin → jede Seite, die dich in einem iframe einbettet, kann Nachrichten lesen oder senden. Siehe Sicherheit.

Spickzettel

// 1. Die Komponente: Inputs mit Transform für Attribute, Outputs mit spezifischen Namen
readonly maxStars = input(5, { transform: numberAttribute }); // <x max-stars="7">
readonly ratingChange = output<RatingChange>();               // CustomEvent, detail

// 2. Definition: nur einmal, Tag ≠ Selektor, Tag mit Bindestrich
createApplication({ providers: [] }).then(({ injector }) => { // eigenes Bundle
  // (in einer bestehenden Anwendung: const injector = inject(ApplicationRef).injector)
  if (!customElements.get('atlas-rating')) {
    customElements.define('atlas-rating', createCustomElement(AtlasRating, { injector }));
  }
});

// 3. Verwendung
el.setAttribute('max-stars', '7');       // string → transform
el.maxStars = 7;                         // beliebiger Typ
el.addEventListener('ratingChange', fn); // am Element: bubbles false, composed false

// 4. Lifecycle: append = create (synchron); remove = destroy nach 10 ms;
//    zerstörte Instanz nicht erneut anhängen → ein anderes Element erzeugen

// 5. Styles: ViewEncapsulation.ShadowDom + CSS-Variablen + ::part() fürs Theming

// 6. Kommunikation: Attribute/Properties/Events → CustomEvent auf window (typisiert)
//    → BroadcastChannel (Tabs, gleiche Origin) → postMessage + Origin (iframes)

Kap. 13 · 0 von 4 abgeschlossen

Lektion abgeschlossen?

Markiere sie, dann erscheint sie auf deiner Karte: im Inhaltsverzeichnis und auf der Startseite.

Wird automatisch markiert, sobald du die ganze Lektion gelesen hast.

  1. Micro Frontends
  2. Web Components mit Angular Elements
  3. Monorepo, Bibliotheken & Grenzen
  4. Hexagonale Architektur (Ports & Adapters)
Deine ganze Karte ansehen

Alle 75 Seiten · tippen zum Filtern

  • 01Was ist neu in Angular 21 & 22Alle wichtigen Änderungen in v21 und v22 in Kürze, mit Link zur Seite, die sie jeweils erklärt.Start
  • 01Deine erste App, Schritt für SchrittEine Leseliste, von einer leeren Komponente bis zur fertigen App — Signals, @for, Inputs und Outputs, ein Service und ein Formular, Schritt für Schritt.Start
  • 01Schnelle Entscheidungen„Wohin damit?“, „Signal oder Observable?“, „Shared oder Feature?“ — kurze Antworten auf Alltagsfragen.Start
  • 01Angular-Interviewfragen60 echte Interviewfragen für Junior, Mid und Senior, mit der passenden Antwort, der Nachfrage und den Warnsignalen. Dazu ein Probeinterview.Start
  • 02Standalone & BootstrapWie eine App ohne NgModule startet: bootstrapApplication, app.config.ts, imports in der Komponente.Moderne Grundlagen
  • 02Data Binding & EventsInterpolation, Property- und Attribut-Bindings, Events mit $event und Tastenfiltern und wie [( )] aufgebaut ist — was in das DOM geht und was zurückkommt.Moderne Grundlagen
  • 02Control Flow: @if, @for, @switchDie Block-Syntax, die *ngIf/*ngFor/ngSwitch ersetzt, mit track, @empty und Exhaustive Checks.Moderne Grundlagen
  • 02@let und die neuen Template-AusdrückeLokale Variablen mit @let, Arrow Functions, Spread, Regex, instanceof, Kommentare in Tags.Moderne Grundlagen
  • 02@defer — Lazy Loading in TemplatesSchwere Komponenten nur laden, wenn sie gebraucht werden: Trigger, Prefetch, Placeholder, Loading, Error.Moderne Grundlagen
  • 02Class- & Style-BindingsWarum [class.x] und [style.x] ngClass und ngStyle abgelöst haben (mit automatischer Migration ab v21).Moderne Grundlagen
  • 02Komponenten-Styles & Kapselung:host, die drei Kapselungsmodi, Theming mit CSS-Variablen und warum ::ng-deep der letzte Ausweg ist.Moderne Grundlagen
  • 02Pipes: wann und wiePure vs. Impure Pipes, eigene Pipes und wie du zwischen Pipe, computed() und Methode wählst.Moderne Grundlagen
  • 03signal() und computed()Die reaktiven Grundbausteine: beschreibbarer Zustand, abgeleiteter Zustand, Gleichheit, untracked und Regeln zur Immutability.Signals & Reaktivität
  • 03effect() — wann ja, wann neinMit Signals synchronisierte Seiteneffekte: localStorage, Logging, imperative APIs. Und ihre Stolperfallen.Signals & Reaktivität
  • 03linkedSignal() — editierbarer abgeleiteter ZustandEin Signal, das sich zurücksetzt, wenn sich seine Quelle ändert, das du aber auch manuell setzen kannst.Signals & Reaktivität
  • 03resource, rxResource, httpResourceAsynchrone Daten als Signals: status, value, error, reload, automatischer Abbruch. Stabil seit v22.Signals & Reaktivität
  • 03debounced() — verzögerte SignalsDie „Debounce“-Variante eines Signals, ohne RxJS. Perfekt für die Suche.Signals & Reaktivität
  • 03Listen: suchen, filtern, sortieren, blätternEine Kette von computed-Signals, die nur neu berechnet, was sich geändert hat — mit den Filtern in der URL, damit ein Link dieselbe Ansicht öffnet.Signals & Reaktivität
  • 03Observables von null anWas ein Observable ist, subscribe und unsubscribe, die Operatoren, die du jede Woche brauchst, cold vs. hot — und wann ein Signal reicht.Signals & Reaktivität
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — und wann RxJS weiterhin die richtige Wahl bleibt.Signals & Reaktivität
  • 04input() und output()Inputs als Signals (required, transform, alias) und typisierte Outputs. Daten nach unten, Events nach oben.Komponenten: Kommunikation
  • 04model() — Two-Way-BindingBanana-in-a-Box [( )] mit einer einzigen API. Wann du es nutzt und wann input + output besser ist.Komponenten: Kommunikation
  • 04viewChild & contentChildQueries als Signals: viewChild, viewChildren, contentChild, contentChildren, die Option read.Komponenten: Kommunikation
  • 04Lifecycle & DestroyRefWelche Hooks in der Signals-Ära noch zählen, afterNextRender, DestroyRef und typisiertes ngOnChanges.Komponenten: Kommunikation
  • 04Content Projectionng-content mit select, mehrere Slots, Standardinhalt und die Falle der bedingten Projektion.Komponenten: Kommunikation
  • 04ng-template & ngTemplateOutletTemplates als Werte: Render Props in Angular, typisierter Kontext und Anpassen von Komponenten.Komponenten: Kommunikation
  • 05Smart vs. DumbContainer vs. Presentational: wer den Zustand hält, wer nur anzeigt und wie du die beiden sauber trennst.Komponenten-Design
  • 05Generische Komponenten <T>Eine korrekt typisierte DataTable<T> oder Select<T>, mit Templates für Zellen.Komponenten-Design
  • 05Dynamische KomponentenNgComponentOutlet, createComponent mit inputBinding/outputBinding und ein Dashboard, das aus einer Konfiguration gebaut wird.Komponenten-Design
  • 05Direktiven & Host-KompositionAttribut-Direktiven, die host: {}-Metadaten und die Directive Composition API (hostDirectives).Komponenten-Design
  • 05Eigene Formular-ControlsFormValueControl (Signal Forms) vs. ControlValueAccessor: eine Sternebewertung, in beiden Varianten.Komponenten-Design
  • 05Change DetectionOnPush ist ab v22 der Standard, zone.js entfällt ab v21. Was das Rendering auslöst und wie du dir nicht die Finger verbrennst.Komponenten-Design
  • 06inject() & @Service()Der neue @Service()-Decorator ab v22, inject() vs. Konstruktor und der Injection-Kontext.Dependency Injection
  • 06Provider & die Injector-HierarchieRoot vs. Route vs. Komponente, useClass/useValue/useFactory/useExisting und die Modifier optional/self/skipSelf.Dependency Injection
  • 06InjectionToken & provideX()Typisierte Konfiguration, provideFeature()-Funktionen, makeEnvironmentProviders und Initializer.Dependency Injection
  • 06injectAsync() — Lazy ServicesLade einen schweren Service erst, wenn du ihn brauchst, mit Prefetch im Leerlauf.Dependency Injection
  • 07Ordnerstrukturcore / shared / features, was in welchen Ordner gehört und wie ein Feature von innen aussieht.Architektur der Anwendung
  • 07Shared vs. FeatureWann du eine Komponente nach Shared verschiebst und (vor allem) wann NICHT. Eine konkrete Checkliste.Architektur der Anwendung
  • 07Interfaces & ModelleInline-Interface oder eigene Datei? DTO vs. Domänenmodell, type vs. interface, wohin damit.Architektur der Anwendung
  • 07State Management mit SignalsEinfache Signal-Stores, Fassaden, lokaler vs. globaler Zustand und wann du NgRx brauchst.Architektur der Anwendung
  • 07Styleguide & NamensgebungDie aktuellen offiziellen Konventionen: Dateinamen ohne Suffixe, protected, readonly, Reihenfolge der Member.Architektur der Anwendung
  • 07Barrels, Aliase & Grenzenindex.ts, Pfad-Aliase, die öffentliche API eines Features und Abhängigkeitsregeln, die sich automatisch prüfen lassen.Architektur der Anwendung
  • 07Fehler: @boundary & ErrorHandlerDer neue @boundary-Block ab v22.2, der globale ErrorHandler, HTTP-Fehler und Fehler aus resource.Architektur der Anwendung
  • 08Modernes Routing & Lazy LoadingloadComponent, Child Routes, Parameter als Inputs und Parameter-Vererbung (neuer Standard in v22).Routing
  • 08Guards & ResolvercanActivate, canMatch, canDeactivate und resolve als Funktionen, mit Redirects über UrlTree / RedirectCommand.Routing
  • 09Signal Forms — die GrundlagenFormulare auf Basis von Signals: form(), [formField], schemabasierte Validierung. Stabil seit v22.Formulare
  • 09Signal Forms — fortgeschrittenEigene und feldübergreifende Validierung, async/HTTP mit Debounce, dynamische Listen, disabled/hidden, wiederverwendbare Schemas.Formulare
  • 09Typisierte Reactive FormsTypisierte FormGroup, nonNullable, FormArray — und wie sie neben Signal Forms bestehen.Formulare
  • 10HttpClient, Interceptors & die API-SchichtHttpClient ohne Setup (v21), Fetch als Standard (v22), funktionale Interceptors und eine API-Schicht mit DTO-Mapping.HTTP & Daten
  • 11Performance — eine ChecklisteWas wirklich zählt: OnPush + Signals, track, @defer, Lazy Routes, Bilder, Bundles und Hydration.Qualität & Performance
  • 11Testen mit VitestVitest ist ab v21 der Standard-Runner. TestBed, Komponenten mit Signals, Services, HTTP und Harnesses.Qualität & Performance
  • 11DevTools: ein Blick in deine AppChrome DevTools und Angular DevTools Schritt für Schritt: was jedes Panel zeigt, mit Übungen, die auf dieser Website funktionieren.Qualität & Performance
  • 11Angular AriaHeadless, barrierefreie Komponenten, die du nach Belieben stylst: Tabs, Accordion, Menü, Listbox...Qualität & Performance
  • 12SSR, Prerendering & Render ModesWann auf dem Server gerendert wird, wann beim Build und wann nur im Browser: RenderMode pro Route, app.routes.server.ts und die echten Kosten.SSR & Hydration
  • 12Hydration: Event Replay & inkrementellWie der Browser das Server-HTML übernimmt, ohne es neu zu rendern: vollständige Hydration, Event Replay, @defer (hydrate on …) und Mismatch-Fehler.SSR & Hydration
  • 12Servertauglicher Codewindow gibt es auf dem Server nicht: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, der HTTP-Cache und PendingTasks.SSR & Hydration
  • 13Micro FrontendsWann es sich lohnt, eine App in mehrere Deployments aufzuteilen und wann es nur Komplexität ist: Native Federation, Module Federation, iframes, Web Components.Architektur im großen Maßstab
  • 13Web Components mit Angular ElementscreateCustomElement: eine Angular-Komponente, die überall als HTML-Tag nutzbar ist, plus Kommunikation zwischen Apps über Events und BroadcastChannel.Architektur im großen Maßstab
  • 13Monorepo, Bibliotheken & GrenzenEin Workspace mit mehreren Apps und Bibliotheken: ng generate library, Nx, Secondary Entry Points, Publishing und Abhängigkeitsregeln.Architektur im großen Maßstab
  • 13Hexagonale Architektur (Ports & Adapters)Eine Domäne ohne Angular, Ports als abstrakte Klassen und austauschbare Adapter: HTTP, localStorage oder In-Memory, ohne die UI anzufassen.Architektur im großen Maßstab
  • 14Signals unter der HaubeDer reaktive Graph: Producer und Consumer, Push-Pull, warum computed lazy und glitch-frei ist, das Diamond-Problem und wann Effects laufen.Fortgeschrittene Reaktivität
  • 14Fortgeschrittenes RxJS: Operatoren höherer OrdnungswitchMap, mergeMap, concatMap und exhaustMap an einem Live-Diagramm, dazu Retry mit Backoff, shareReplay und wie du Leaks vermeidest.Fortgeschrittene Reaktivität
  • 14NgRx SignalStoreState Management im großen Stil auf Signals: withState, withComputed, withMethods, rxMethod, Entities und Hooks — und wann ein einfacher Store reicht.Fortgeschrittene Reaktivität
  • 14Zustandsautomaten mit SignalsKeine unmöglichen Zustände mehr: ein typisierter Automat auf Signals, mit Übergängen, Guards und einem Live-Zustandsdiagramm.Fortgeschrittene Reaktivität
  • 15WebSockets: Echtzeit, die das Netz überstehtEin typisierter Socket als Signals: Reconnect mit Backoff und Jitter, Heartbeat, ein Postausgang im Offline-Modus und SSR-Sicherheit.Echtzeit & offline
  • 15Optimistische Updates & RollbackEine sofortige UI, die ehrlich bleibt: Snapshot und Rollback, Antworten in falscher Reihenfolge, Idempotenzschlüssel.Echtzeit & offline
  • 15Offline-first: Service Worker & CachingDer Angular Service Worker: Caching-Strategien, Update-Hinweise und ein Postausgang für Änderungen, die offline entstehen.Echtzeit & offline
  • 16Web WorkersSchwere Berechnungen, ohne die UI zu blockieren: ein echter Worker in der Seite, Message Passing und wann es sich nicht lohnt.Fortgeschrittene UI & Performance
  • 16Riesige Listen: Virtual Scroll100.000 Zeilen ohne Ruckeln: CDK Virtual Scroll, itemSize, trackBy und die Alternativen (Paginierung, Infinite Scroll).Fortgeschrittene UI & Performance
  • 16Moderne Animationen & View Transitionsanimate.enter und animate.leave statt des veralteten @angular/animations, CSS-Transitions und View Transitions bei der Navigation.Fortgeschrittene UI & Performance
  • 16Memory Leaks finden und behebenSubscriptions, Timer und Listener, die ihre Komponente überleben – wie du sie in den DevTools findest und schließt.Fortgeschrittene UI & Performance
  • 17Sicherheit: XSS, Sanitizing & CSPWas Angular automatisch gegen XSS tut, wann bypassSecurityTrust gefährlich wird, dazu CSP mit Nonce, Trusted Types und XSRF.Sicherheit & Globalisierung
  • 17Authentifizierung: Tokens & RefreshEin Interceptor, der das Token anhängt, bei 401 einmal erneuert und die wartenden Requests wiederholt — dazu, wo du das Token aufbewahrst.Sicherheit & Globalisierung
  • 17OAuth 2.1 & OpenID Connect mit PKCEDer Authorization-Code-Flow mit PKCE Schritt für Schritt: Verifier und Challenge, State und Nonce, das ID-Token und warum ein BFF sicherer ist.Sicherheit & Globalisierung
  • 17Internationalisierung (i18n)$localize, das i18n-Attribut, ICU plural/select, ein Build pro Sprache vs. Übersetzungen zur Laufzeit und Formatierung nach Locale.Sicherheit & Globalisierung