AngularAtlased. 22.2

Cap. 13Arhitectură la scară58 / 75

Web Components cu Angular Elements

createCustomElement: o componentă Angular folosită ca tag HTML oriunde, plus comunicarea între aplicații prin evenimente și BroadcastChannel.

stabil ~0 min de citit Documentația oficială

Un custom element (Web Component) e un tag HTML definit de tine — <atlas-rating> — pe care browserul îl recunoaște oriunde: într-o pagină statică, într-un CMS, într-o aplicație React sau într-o altă aplicație Angular. @angular/elements face puntea: createCustomElement() transformă o componentă Angular într-o clasă care extinde HTMLElement. Input-urile devin atribute și proprietăți, output-urile devin CustomEvent-uri, iar componenta se creează singură când tag-ul intră în DOM.

Exemplele de pe pagină sunt reale: definim elementele la runtime cu customElements.define, le creăm cu document.createElement într-un container pe care Angular nu-l gestionează și le ascultăm cu addEventListener — exact cum ar face o pagină care nu știe nimic despre Angular. Totul rulează în aplicația asta, care e zoneless.

Pe scurt
  • createCustomElement transformă o componentă într-un Web Component, folosibil în orice pagină: React, Vue, un CMS sau HTML simplu.
  • Input-urile devin proprietăți și atribute (în kebab-case), iar output-urile devin evenimente CustomEvent.
  • Cu Shadow DOM, stilurile elementului și ale paginii nu se mai amestecă.
  • Fiecare element aduce runtime-ul Angular. Mai multe elemente pe aceeași pagină ar trebui să vină din același bundle.
  • Merită pentru widget-uri incluse în pagini pe care nu le controlezi, nu pentru o aplicație Angular obișnuită.
De ce contează

Angular Elements e unealta potrivită când o componentă trebuie să trăiască în afara unei aplicații Angular: widget-uri pentru un CMS sau pentru site-urile clienților, un design system folosit de echipe pe framework-uri diferite, micro frontends care nu pot împărți aceeași versiune de Angular. În interiorul unei aplicații Angular obișnuite e aproape mereu alegerea greșită: pierzi verificarea de tipuri în template, ierarhia de DI și proiecția de conținut dinamică.

Ce face createCustomElement

createCustomElement(Component, { injector }) nu afișează nimic. Întoarce o clasă (NgElementConstructor) pe care o înregistrezi cu customElements.define(tag, clasa). Clasa citește metadatele componentei (reflectComponentType) și pregătește trei lucruri:

  • observedAttributes — numele input-urilor în kebab-case (maxStars → max-stars), ca browserul să-i anunțe fiecare schimbare de atribut;
  • un getter și un setter pe prototip pentru fiecare input, deci el.maxStars = 7 funcționează ca o proprietate DOM obișnuită;
  • o strategie (NgElementStrategy) care, la connectedCallback, apelează createComponent cu elementul ca hostElement, atașează view-ul la ApplicationRef și rulează un prim detectChanges() — sincron, înainte ca append() să se întoarcă.

Injectorul pe care îl dai e locul din care componenta își ia serviciile (HttpClient, store-uri, token-uri de configurare). Într-un bundle dedicat widget-urilor îl obții din createApplication(), o aplicație fără componentă root; într-o aplicație existentă, din 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',               // ≠ numele tag-ului (vezi mai jos)
  encapsulation: ViewEncapsulation.ShadowDom, // CSS-ul paginii gazdă nu intră
  template: `
    @for (s of stars(); track s) {
      <button type="button" [class.on]="s <= current()" [disabled]="readonly()"
              (click)="pick(s)">★</button>
    }
  `,
})
export class AtlasRating {
  // atribute: value, max-stars, readonly
  readonly value = input(0, { transform: numberAttribute });
  readonly maxStars = input(5, { transform: numberAttribute });
  readonly readonly = input(false, { transform: booleanAttribute });

  // devine CustomEvent 'ratingChange', valoarea în event.detail
  readonly ratingChange = output<RatingChange>();

  // stare locală, resetată când gazda trimite altă valoare
  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 });
  }
}
Tag-ul nu e selectorul

Ghidul oficial cere explicit: nu folosi selectorul componentei ca nume de tag. Dacă atlas-rating e și selector, și custom element, iar componenta e importată și într-un template, Angular creează două instanțe pe același element. De aici convenția atlas-rating-cmp (selector) vs. atlas-rating (tag). Tag-ul trebuie să conțină o cratimă, iar define() pe un nume deja înregistrat aruncă NotSupportedError — de aceea verifici întâi cu customElements.get().

Zone.js nu e necesar. Din v19 elementele folosesc scheduler-ul obișnuit de change detection (changelog 19.0.0: „switch to ComponentRef.setInput & remove custom scheduler”), iar din v21 aplicațiile sunt zoneless implicit. Un setAttribute ajunge la componentRef.setInput(), care marchează view-ul și programează o randare; un click în template-ul elementului schimbă un semnal — exact ca într-o componentă OnPush obișnuită.

Definire într-o aplicație existentă

Cazul tipic: aplicația primește HTML dintr-un CMS și îl inserează cu innerHTML. Componentele Angular din acel HTML nu ar porni niciodată, dar custom elements pornesc singure, pentru că browserul le recunoaște tag-ul.

// Într-o aplicație existentă (ex. HTML din CMS, inserat cu 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; // root: trăiește cât aplicația
  // define() pe un nume deja înregistrat aruncă NotSupportedError
  if (!customElements.get('atlas-rating')) {
    customElements.define('atlas-rating', createCustomElement(AtlasRating, { injector }));
  }
}

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

Atribute, proprietăți, evenimente

Un element are două „intrări” pentru același input. Atributul (max-stars="7") e mereu un string — de aceea input-urile numerice sau booleene au nevoie de transform: numberAttribute / booleanAttribute; fără el, maxStars() + 1 devine '71'. Proprietatea (el.maxStars = 7) primește orice tip, deci obiectele și array-urile trec doar pe aici. La citire, el.maxStars întoarce valoarea curentă a input-ului; din v21 asta e valabil și pentru input() pe semnale (înainte trebuia apelat el.maxStars()).

Input / outputAtribut HTMLProprietate JSDe reținut
maxStars
transform: numberAttribute
max-stars="7"el.maxStars = 7atributul vine ca '7', transform-ul îl face număr
readonly
transform: booleanAttribute
readonlyel.readonly = trueremoveAttribute trimite null → false
userId
alias: 'user'
user="42"el.userIdatributul folosește alias-ul, proprietatea — numele din clasă
items
input<Item[]>
—el.items = [...]prin atribut ar ajunge "[object Object]"
ratingChange
output<T>()
—addEventListener('ratingChange', …)CustomEvent, valoarea în event.detail
export class Widget {
  // atributul = numele input-ului (sau alias-ul) în 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[]>([]);                               // doar proprietate

  // un output devine CustomEvent cu numele output-ului (sau al alias-ului)
  readonly ratingChange = output<RatingChange>();           // 'ratingChange'
  readonly closed = output<void>({ alias: 'atlasClosed' }); // 'atlasClosed'
}

// Ce generează createCustomElement pentru clasa de mai sus:
//   static observedAttributes = ['max-stars', 'compact', 'user', 'items']
//   getter/setter pe prototip:  maxStars, compact, userId, items
//   (proprietatea folosește numele din clasă, atributul folosește alias-ul)

Evenimentul e creat cu new CustomEvent(nume, { detail }) — fără bubbles și fără composed. Un listener pe document sau pe containerul părinte nu primește nimic; asculți direct pe element. În exemplul de mai jos containerul are și el un listener pentru ratingChange: n-o să apară niciodată în log.

LiveElement real: <atlas-rating> definit la runtime
Pagina gazdă (cod DOM simplu)
Trimite ca
// apasă un buton sau schimbă un câmp
Container pe care Angular nu-l gestionează
în DOM: nucomponente create: 0distruse: 0citit el.value: —
Ce se întâmplă (addEventListener + callback-uri)
  • 09:11:45 customElements.define('atlas-rating', …) — definit acum
Schimbă câmpurile în modul „atribut”, apoi în „proprietate”, și urmărește comanda trimisă și ce citește el.value. Click pe stele: log-ul arată event.detail și bubbles=false. „Mută” păstrează starea; el.remove() urmat de „append(aceeași instanță)” arată capcana descrisă mai jos.

Observă un detaliu: după ce dai click pe o stea, el.value rămâne valoarea primită de la gazdă. Input-ul e un canal într-un singur sens; componenta ține alegerea într-un linkedSignal și o anunță prin eveniment. Dacă gazda vrea să fie sursa adevărului, ascultă evenimentul și scrie înapoi el.value — modelul „controlled component”.

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

document.addEventListener('ratingChange', handler); // ✗ nu primește nimic

// Dacă gazda chiar are nevoie de delegare (un listener pentru 50 de widget-uri),
// emite explicit, din componentă, un eveniment care urcă și trece de 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 }),
    );
  }
}

Ciclul de viață

Componenta se naște la connectedCallback și moare la disconnectedCallback, dar nu imediat: strategia programează destroy() peste 10 ms (DESTROY_DELAY = 10 în sursa pachetului). Dacă elementul revine în DOM înainte — o mutare remove() + append() în același task, ca la sortarea unei liste — distrugerea se anulează și starea internă rămâne. Input-urile setate înainte de conectare sunt ținute minte și aplicate la creare. Mai mult, proprietățile setate pe un element încă nedefinit (scriptul widget-ului nu s-a încărcat) sunt preluate la upgrade, deci gazda poate seta el.items oricând.

document.createElement('atlas-rating')
    constructor; încă nicio componentă (input-urile setate acum sunt ținute minte)
container.append(el)
    connectedCallback → createComponent(...) SINCRON, attachView(appRef), detectChanges()
el.setAttribute('value', '4')
    attributeChangedCallback → componentRef.setInput('value', '4') → transform → CD
el.value = 4
    setter pe prototip → același drum, fără conversie la string
el.remove()
    disconnectedCallback → destroy PROGRAMAT peste 10 ms
    ↳ re-adăugat în < 10 ms (o mutare)  destroy anulat: aceeași instanță, aceeași stare
    ↳ după 10 ms                        componentRef.destroy() → DestroyRef.onDestroy
container.append(el)   // aceeași instanță, după destroy
    ⚠︎ componenta NU se recreează: HTML mort + NG0953 în consolă
document.createElement('atlas-rating')
    ✓ instanță nouă, curată
Re-atașarea unui element deja distrus

Ghidul oficial avertizează (secțiunea Limitations, issue #38778) că detașarea și re-atașarea unui element creat cu @angular/elements e problematică. În v22.2.2, comportamentul se vede în demo: după ce componenta a fost distrusă, un nou append al aceleiași instanțe nu o mai recreează. HTML-ul rămas în shadow root e mort (click-urile nu fac nimic), el.value e undefined, iar Angular raportează NG0953 (abonare la un OutputRef distrus). O a doua încercare aruncă NotSupportedError din attachShadow, pentru că un element nu poate avea două shadow roots.

Apare când cineva reutilizează nodul DOM: ng-if din AngularJS, un editor de CMS care mută blocuri, un cache de elemente în codul tău. Soluția: nu reutiliza instanța — creează alt element cu document.createElement — sau, dacă doar vrei să-l ascunzi, lasă-l în DOM cu atributul hidden.

Shadow DOM și stiluri

Cu Emulated (implicit), Angular rescrie selectorii componentei cu atribute unice: stilurile tale nu ies, dar CSS-ul paginii intră. Un widget pus pe un site străin moștenește orice regulă de tipul .content p sau button { text-transform: uppercase !important } — cel mai frecvent bug la widget-uri. Cu ViewEncapsulation.ShadowDom, template-ul se randează într-un shadow root (mod open), iar selectorii paginii nu mai văd înăuntru. Trec doar proprietățile moștenite (font, color) și variabilele CSS — exact ce îți trebuie pentru o temă controlată.

O nuanță puțin cunoscută: în modul ShadowDom, Angular copiază în fiecare shadow root și stilurile celorlalte componente din aceeași aplicație. Pentru componentele Emulated e doar balast (selectorii lor nu se potrivesc), dar o componentă cu ViewEncapsulation.None ajunge să stilizeze și interiorul widget-ului. ExperimentalIsolatedShadowDom (experimental din v21) pune în shadow root doar stilurile componentei. La SSR, ambele moduri shadow sunt randate ca Emulated.

EncapsulareCSS-ul paginii intră?Stiluri None din aplicațieVariabile CSS
Emulated (implicit)dadada
ShadowDomnuda (copiate în shadow root)da
ExperimentalIsolatedShadowDomexperimentalnunuda
LiveAcelași widget, trei encapsulări
Elementel.shadowRoot<style> în shadow root
Bifează pe rând căsuțele. CSS-ul „CMS” strică doar varianta Emulated. Componenta None ajunge și în ShadowDom (numărul de style crește cu unu), dar nu și în varianta izolată. Variabila CSS le tematizează pe toate trei.

Shadow DOM schimbă mai mult decât stilurile: evenimentele care ies din shadow root sunt retargetate (event.target devine host-ul), document.querySelector nu găsește nimic din interior, iar testele e2e trebuie să străpungă shadow root-ul. Pentru temă, expune variabile CSS și, unde e nevoie de mai mult control, atribute part stilizabile cu ::part().

@Component({
  selector: 'atlas-card-cmp',
  encapsulation: ViewEncapsulation.ShadowDom,
  template: `
    <h3 part="title">{{ title() }}</h3>
    <ng-content />
  `,
  styles: `
    :host { display: block; }
    /* variabilele CSS și proprietățile moștenite (font, color) TREC în shadow root */
    h3 { color: var(--atlas-accent, #2b45d4); font: inherit; font-weight: 650; }
  `,
})
export class AtlasCard {
  readonly title = input('');
}

Build, bundle, distribuție

Ca să folosești elementele în afara unei aplicații Angular, ai nevoie de un build separat: o aplicație al cărei main.ts apelează createApplication() și customElements.define(), fără componentă root. Schematic-ul Native Federation are chiar opțiunea --webcomponent, care generează exact acest bootstrap (vezi Micro frontends).

Costul e runtime-ul. Un singur element simplu, construit cu Angular 22.2.2 în producție, zoneless, iese ≈ 110 kB minificat, ≈ 33 kB transferați (estimarea CLI) — practic @angular/core + platform-browser + un pic de RxJS. Trei widget-uri din trei build-uri diferite pe aceeași pagină înseamnă trei copii ale runtime-ului, trei injectoare și trei aplicații care nu împart niciun serviciu.

  • Grupează widget-urile unei echipe într-un singur bundle: un createApplication(), mai multe define().
  • Nume stabile pentru integrare prin <script src>: --output-hashing=none și foldere versionate (/1.4.0/main.js), ca să poți pune cache lung fără hash în nume.
  • Lazy în interior: @defer și import() funcționează și într-un element, deci partea grea a widget-ului poate veni la nevoie.
# un proiect separat în workspace, doar pentru widget-uri
ng generate application atlas-widgets --ssr=false
# main.ts: createApplication + customElements.define (vezi main.ts mai sus)

# nume de fișier stabile, ca un CMS să poată include <script src=".../main.js">
ng build atlas-widgets --output-hashing=none
# → publică în foldere versionate: /atlas-widgets/1.4.0/main.js
#   (cache lung, fără hash în nume; chunk-urile lazy își păstrează hash-ul)

# măsurat cu Angular 22.2.2, un element simplu, zoneless, build de producție:
#   main.js ≈ 110 kB minificat, ≈ 33 kB transfer estimat de CLI
# fiecare bundle construit separat își aduce propria copie de
# @angular/core + @angular/platform-browser

Consumul din alte medii

Pentru HTML static ajunge un <script type="module"> și tag-ul. Celelalte medii au fiecare un mic detaliu de configurare:

  • Angular: componenta gazdă declară schemas: [CUSTOM_ELEMENTS_SCHEMA]. Binding-urile [prop] și (event) funcționează, dar fără verificare de tipuri — dacă ești în aceeași aplicație, folosește componenta direct.
  • React 19 are suport complet pentru custom elements: props-urile care corespund unei proprietăți a elementului sunt setate ca proprietăți DOM (în versiunile anterioare ajungeau ca atribute). Pentru evenimente custom, varianta portabilă e un ref + addEventListener.
  • Vue 3 trebuie să știe că tag-ul nu e o componentă Vue: opțiunea de compilare isCustomElement.
  • CMS-uri: editorul inserează doar HTML, deci totul trebuie să se poată configura prin atribute string. Proiectează input-urile ca primitive; pentru configurații complexe, un atribut JSON cu un transform care face JSON.parse.
// Să folosești un custom element (al tău sau al altei echipe) într-o componentă Angular
import { CUSTOM_ELEMENTS_SCHEMA, Component, signal } from '@angular/core';

@Component({
  selector: 'app-product',
  // fără schema: eroare „'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] setează proprietatea DOM, (ratingChange) face addEventListener —
// dar fără nicio verificare de tipuri.

Conținutul unui custom element apare abia după ce scriptul se descarcă și rulează. Pentru text esențial pentru SEO sau pentru LCP (titlul paginii, prețul, conținutul principal), un widget client-side e alegerea greșită — vezi SSR.

Comunicare între aplicații

Când pe aceeași pagină trăiesc mai multe aplicații independente (widget-uri ale unor echipe diferite, micro frontends), ele nu au un injector comun. Rămân mecanismele platformei, în ordinea în care merită încercate:

MecanismRazăCe poți trimiteAtenție la
Atribute / proprietăți + evenimente pe elementgazda ↔ elementul eiorice (proprietăți), detail (evenimente)e contractul public al widget-ului: versionează-l
CustomEvent pe windowaceeași paginăorice obiect (referință, nu copie)nume cu prefix, „late subscriber”, nu e stare
BroadcastChanneltab-uri, ferestre, iframe-uri, workers — aceeași originevalori clonabile (structured clone)nu ajunge la expeditor; close() la final
postMessageiframe / fereastră, inclusiv altă originevalori clonabiletargetOrigin explicit, verifică event.origin

Un bus pe window e cel mai simplu contract între aplicații de pe aceeași pagină: nume de evenimente cu prefix (atlas:cart:add), tipuri într-un pachet mic și versionat, două funcții emit/listen. Capcana clasică e că un eveniment nu e stare: o aplicație montată mai târziu a ratat tot ce s-a întâmplat înainte. Soluția e un handshake — la pornire ceri starea, iar proprietarul ei răspunde.

// contracts/atlas-events.ts — un pachet mic, versionat, împărțit de echipe
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);
}
LiveDouă aplicații pe aceeași pagină, un bus pe window
Aplicația A · echipa Catalog în coș: ?
Laptop Pro 14
Căști Studio
Mouse Ergo
Aplicația B · echipa Coș 0 produse · 0 lei
Coșul e gol.
Trafic pe window (un al treilea ascultător, independent)
  • Adaugă un produs din A.
Adaugă produse din A: B deține coșul și anunță atlas:cart:changed. Debifează „A cere starea la pornire”, apoi demontează și remontează A: nu mai știe câte produse sunt. Cu handshake-ul bifat, B răspunde imediat. Demontează B: A continuă să emită, fără nicio eroare.

BroadcastChannel leagă toate contextele de aceeași origine care deschid un canal cu același nume: tab-uri, ferestre, iframe-uri, workers. Mesajul e copiat prin structured clone (fără funcții, fără instanțe de clase cu metode) și nu ajunge la obiectul care l-a trimis. Cazuri tipice: logout în toate tab-urile, sincronizarea coșului, invalidarea unui cache. Pentru iframe-uri de altă origine rămâne postMessage, cu origine verificată la ambele capete.

LiveBroadcastChannel între tab-uri
Tab-ul ăsta: #nbezAlte tab-uri văzute: niciunul acumDeschide pagina într-un tab nou ↗
Stare partajată
0
ultima scriere: —
Mesaj
Expeditorul nu își primește propriul mesaj — doar celelalte tab-uri.
  • Deschide un al doilea tab și apasă +1 acolo.
Deschide pagina într-un al doilea tab (butonul de sus), pune tab-urile unul lângă altul și apasă +1 sau trimite un mesaj. Un tab nou primește starea curentă prin handshake; la închidere, celelalte primesc „bye”.
// Aceeași origine: alte tab-uri, ferestre, iframe-uri, 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 {
    // ajunge la celelalte tab-uri, NU și la acest obiect
    this.channel.postMessage({ type: 'logout' });
  }
}

Când DA, când NU

Angular Elements
  • Widget-uri pentru pagini pe care nu le controlezi: CMS (WordPress, AEM, Sitecore), site-urile clienților, un chat de suport, un configurator de produs.
  • Un design system folosit din mai multe framework-uri, când echipa lui scrie Angular.
  • Insule interactive într-o aplicație server-side (PHP, Java, .NET) care nu se rescrie curând.
  • Micro frontends care nu pot împărți aceeași versiune de Angular: fiecare element își aduce runtime-ul, deci versiunile pot diferi.
  • HTML dinamic (din CMS, din markdown) inserat cu innerHTML într-o aplicație Angular, unde componentele obișnuite nu ar porni.
Altceva
  • În interiorul aceleiași aplicații Angular → componente obișnuite (sau componente dinamice cu createComponent/NgComponentOutlet).
  • Conținut critic pentru SEO sau LCP → randare pe server (SSR, SSG), nu un widget client-side.
  • Mai multe aplicații Angular de aceeași versiune care trebuie să împartă runtime-ul → Native Federation, nu N bundle-uri separate.
  • Doar ca să refolosești cod între aplicațiile Angular ale aceleiași echipe → o librărie într-un monorepo.
  • Widget-uri minuscule pe pagini sensibile la greutate (un buton „share”) → 33 kB de runtime nu se justifică; un web component scris de mână sau Lit e mai potrivit.

Capcane frecvente

  • Selectorul componentei folosit ca tag → două instanțe pe același element. Folosește un selector separat (atlas-rating-cmp).
  • define() apelat de două ori (HMR, teste, două bundle-uri care definesc același tag) → NotSupportedError. Verifică customElements.get(); între echipe, prefixează tag-urile cu numele produsului.
  • Așteptarea că evenimentele urcă → listener pe document care nu primește nimic. Ascultă pe element sau re-emite explicit cu bubbles: true, composed: true.
  • Output-uri numite change, input, click → se amestecă cu evenimentele native ale elementelor din interior care ajung la host. Folosește nume specifice (ratingChange).
  • Input numeric fără transform → atributul vine string și maxStars() + 1 devine '71'. Pune numberAttribute / booleanAttribute pe orice input configurabil din HTML.
  • Obiecte prin atribute → componenta primește "[object Object]". Obiectele și array-urile se trimit ca proprietăți.
  • Așteptarea că elementul vede serviciile componentei părinte → el folosește injectorul dat la createCustomElement, nu locul din DOM unde l-ai pus. Serviciile dintr-un providers de componentă sau de rută nu ajung la el.
  • Copii adăugați după creare → nodurile pentru <ng-content> se colectează o singură dată, la conectare. Conținutul dinamic se trimite prin proprietăți.
  • Reutilizarea unui element distrus → HTML mort, NG0953, apoi NotSupportedError. Creează un element nou.
  • Listener-e și canale neînchise → după fiecare montare, încă un window.addEventListener sau încă un BroadcastChannel deschis. Cleanup în DestroyRef.onDestroy.
  • postMessage(data, '*') și fără verificarea event.origin → orice pagină care te încadrează într-un iframe poate citi sau trimite mesaje. Vezi Securitate.

Fișă rapidă

// 1. Componenta: input-uri cu transform pentru atribute, output-uri cu nume specifice
readonly maxStars = input(5, { transform: numberAttribute }); // <x max-stars="7">
readonly ratingChange = output<RatingChange>();               // CustomEvent, detail

// 2. Definirea: o singură dată, tag ≠ selector, tag cu cratimă
createApplication({ providers: [] }).then(({ injector }) => { // bundle separat
  // (într-o aplicație existentă: const injector = inject(ApplicationRef).injector)
  if (!customElements.get('atlas-rating')) {
    customElements.define('atlas-rating', createCustomElement(AtlasRating, { injector }));
  }
});

// 3. Consumul
el.setAttribute('max-stars', '7');       // string → transform
el.maxStars = 7;                         // orice tip
el.addEventListener('ratingChange', fn); // pe element: bubbles false, composed false

// 4. Ciclul de viață: append = create (sincron); remove = destroy după 10 ms;
//    nu re-atașa o instanță distrusă → creează alt element

// 5. Stiluri: ViewEncapsulation.ShadowDom + variabile CSS + ::part() pentru temă

// 6. Comunicare: atribute/proprietăți/evenimente → CustomEvent pe window (tipat)
//    → BroadcastChannel (tab-uri, aceeași origine) → postMessage + origin (iframes)

Cap. 13 · 0 din 4 terminate

Ai terminat lecția?

Marcheaz-o și apare pe harta ta: în cuprins și pe prima pagină.

Se marchează singură după ce citești toată lecția.

  1. Micro frontends
  2. Web Components cu Angular Elements
  3. Monorepo, librării & granițe
  4. Arhitectură hexagonală (ports & adapters)
Vezi toată harta ta

Toate cele 75 de pagini · scrie ca să filtrezi

  • 01Ce e nou în Angular 21 & 22Toate schimbările importante din v21 și v22, pe scurt, cu link către pagina care le explică.Start
  • 01Prima ta aplicație, pas cu pasO listă de lecturi construită de la o componentă goală la o aplicație terminată — signals, @for, input-uri și output-uri, un serviciu și un formular, pas cu pas.Start
  • 01Decizii rapide„Unde pun asta?”, „Signal sau Observable?”, „Shared sau feature?” — răspunsuri scurte la întrebările de zi cu zi.Start
  • 01Întrebări de interviu Angular60 de întrebări reale de interviu pentru junior, mid și senior, cu răspunsul de dat, întrebarea care urmează și greșelile de evitat. Plus o simulare de interviu.Start
  • 02Standalone & bootstrapCum pornește o aplicație fără NgModule: bootstrapApplication, app.config.ts, imports în componentă.Fundamente moderne
  • 02Data binding și evenimenteInterpolare, binding pe proprietăți și pe atribute, evenimente cu $event și filtre de taste și cum e construit [( )] — ce intră în DOM și ce se întoarce.Fundamente moderne
  • 02Control flow: @if, @for, @switchSintaxa de blocuri care înlocuiește *ngIf/*ngFor/ngSwitch, cu track, @empty și verificare exhaustivă.Fundamente moderne
  • 02@let și expresiile noi din templateVariabile locale cu @let, arrow functions, spread, regex, instanceof, comentarii în tag-uri.Fundamente moderne
  • 02@defer — lazy loading în templateÎncarci componente grele doar când e nevoie: triggere, prefetch, placeholder, loading, error.Fundamente moderne
  • 02Class & style bindingsDe ce [class.x] și [style.x] au înlocuit ngClass și ngStyle (cu migrare automată din v21).Fundamente moderne
  • 02Stilurile componentelor și încapsularea:host, cele trei moduri de încapsulare, teme cu variabile CSS și de ce ::ng-deep e ultima soluție.Fundamente moderne
  • 02Pipes: când și cumPipe pur vs impur, pipe custom și cum alegi între pipe, computed() și o metodă.Fundamente moderne
  • 03signal() și computed()Primitivele reactive: stare scriibilă, stare derivată, egalitate, untracked și reguli de imutabilitate.Signals & reactivitate
  • 03effect() — când DA, când NUEfecte secundare sincronizate cu semnale: localStorage, logging, API-uri imperative. Și capcanele lor.Signals & reactivitate
  • 03linkedSignal() — stare derivată editabilăUn semnal care se resetează când se schimbă sursa, dar pe care îl poți și seta manual.Signals & reactivitate
  • 03resource, rxResource, httpResourceDate asincrone ca semnale: status, value, error, reload, anulare automată. Stabile din v22.Signals & reactivitate
  • 03debounced() — semnale cu întârziereVarianta „debounce” a unui semnal, fără RxJS. Perfect pentru căutare.Signals & reactivitate
  • 03Liste: căutare, filtrare, sortare, paginareUn lanț de computed-uri care recalculează doar ce s-a schimbat, cu filtrele ținute în URL, ca un link să redeschidă aceeași vedere.Signals & reactivitate
  • 03Observables de la zeroCe e un Observable, subscribe și unsubscribe, operatorii pe care îi folosești în fiecare săptămână, cold vs hot — și când ajunge un signal.Signals & reactivitate
  • 03Signals ↔ RxJStoSignal, toObservable, takeUntilDestroyed, outputFromObservable — și când RxJS rămâne alegerea bună.Signals & reactivitate
  • 04input() și output()Inputuri ca semnale (required, transform, alias) și outputuri tipizate. Data down, events up.Componente: comunicare
  • 04model() — two-way bindingBanana-in-a-box [( )] cu un singur API. Când îl folosești și când e mai bine input + output.Componente: comunicare
  • 04viewChild & contentChildInterogări ca semnale: viewChild, viewChildren, contentChild, contentChildren, opțiunea read.Componente: comunicare
  • 04Lifecycle & DestroyRefCe hook-uri mai contează în era signals, afterNextRender, DestroyRef și ngOnChanges tipizat.Componente: comunicare
  • 04Content projectionng-content cu select, sloturi multiple, conținut implicit și capcana proiecției condiționale.Componente: comunicare
  • 04ng-template & ngTemplateOutletTemplate-uri ca valori: render props în Angular, context tipizat și personalizare de componente.Componente: comunicare
  • 05Smart vs DumbContainer vs presentational: cine ține starea, cine doar afișează, și cum le împarți corect.Design de componente
  • 05Componente generice <T>Un DataTable<T> sau Select<T> tipizat corect, cu template-uri pentru celule.Design de componente
  • 05Componente dinamiceNgComponentOutlet, createComponent cu inputBinding/outputBinding și un dashboard construit din config.Design de componente
  • 05Directive & host compositionDirective atribut, metadata host: {} și Directive Composition API (hostDirectives).Design de componente
  • 05Controale custom de formularFormValueControl (Signal Forms) vs ControlValueAccessor: un rating cu stele, în ambele variante.Design de componente
  • 05Change detectionOnPush e implicit din v22, zone.js lipsește din v21. Ce declanșează randarea și cum să nu te arzi.Design de componente
  • 06inject() & @Service()Noul decorator @Service() din v22, inject() vs constructor și contextul de injecție.Dependency Injection
  • 06Providers & ierarhia de injectoriRoot vs rută vs componentă, useClass/useValue/useFactory/useExisting și modificatorii optional/self/skipSelf.Dependency Injection
  • 06InjectionToken & provideX()Configurare tipizată, funcții provideFeature(), makeEnvironmentProviders și inițializatori.Dependency Injection
  • 06injectAsync() — servicii lazyÎncarci un serviciu greu abia când ai nevoie de el, cu prefetch la idle.Dependency Injection
  • 07Structura de folderecore / shared / features, ce intră în fiecare și cum arată un feature pe dinăuntru.Arhitectura aplicației
  • 07Shared vs featureCând muți o componentă în shared și (mai ales) când NU. Checklist concret.Arhitectura aplicației
  • 07Interfețe & modeleInterfață inline sau fișier separat? DTO vs model de domeniu, type vs interface, unde le pui.Arhitectura aplicației
  • 07State management cu signalsStore-uri simple pe semnale, facade, stare locală vs globală și când îți trebuie NgRx.Arhitectura aplicației
  • 07Style guide & namingConvențiile oficiale actuale: nume de fișiere fără sufixe, protected, readonly, ordinea membrilor.Arhitectura aplicației
  • 07Barrels, aliasuri & granițeindex.ts, path aliases, API public pe feature și reguli de dependență care se pot verifica automat.Arhitectura aplicației
  • 07Erori: @boundary & ErrorHandlerNoul bloc @boundary din v22.2, ErrorHandler global, erori HTTP și erori din resource.Arhitectura aplicației
  • 08Routing modern & lazy loadingloadComponent, rute copil, parametri ca input-uri și moștenirea parametrilor (default nou în v22).Routing
  • 08Guards & resolverscanActivate, canMatch, canDeactivate și resolve ca funcții, cu redirect prin UrlTree / RedirectCommand.Routing
  • 09Signal Forms — bazeleFormulare construite pe semnale: form(), [formField], validare prin schemă. Stabile din v22.Formulare
  • 09Signal Forms — avansatValidare custom și cross-field, async/HTTP cu debounce, liste dinamice, disabled/hidden, scheme reutilizabile.Formulare
  • 09Reactive Forms tipizateFormGroup tipizat, nonNullable, FormArray — și cum conviețuiesc cu Signal Forms.Formulare
  • 10HttpClient, interceptors & API layerHttpClient fără setup (v21), Fetch implicit (v22), interceptori funcționali și stratul de API cu mapare DTO.HTTP & date
  • 11Performanță — checklistCe chiar contează: OnPush + signals, track, @defer, lazy routes, imagini, bundle și hidratare.Calitate & performanță
  • 11Testare cu VitestVitest e runner-ul implicit din v21. TestBed, componente cu semnale, servicii, HTTP și harness-uri.Calitate & performanță
  • 11DevTools: cum te uiți în aplicațieChrome DevTools și Angular DevTools pas cu pas: ce arată fiecare panou, cu exerciții care merg chiar pe site-ul ăsta.Calitate & performanță
  • 11Angular AriaComponente headless, accesibile, pe care le stilizezi cum vrei: tabs, accordion, menu, listbox...Calitate & performanță
  • 12SSR, prerender & render modesCând randezi pe server, când la build și când doar în browser: RenderMode pe rută, app.routes.server.ts și costurile reale.SSR & hidratare
  • 12Hidratare: event replay & incrementalCum preia browserul HTML-ul de pe server fără să-l refacă: hidratare completă, event replay, @defer (hydrate on …) și erorile de nepotrivire.SSR & hidratare
  • 12Cod compatibil cu serverulwindow nu există pe server: PLATFORM_ID, afterNextRender, DOCUMENT, TransferState, cache-ul HTTP și PendingTasks.SSR & hidratare
  • 13Micro frontendsCând merită să spargi o aplicație în mai multe deploy-uri și când e doar complexitate: Native Federation, Module Federation, iframes, Web Components.Arhitectură la scară
  • 13Web Components cu Angular ElementscreateCustomElement: o componentă Angular folosită ca tag HTML oriunde, plus comunicarea între aplicații prin evenimente și BroadcastChannel.Arhitectură la scară
  • 13Monorepo, librării & granițeUn workspace cu mai multe aplicații și librării: ng generate library, Nx, entry points secundare, publicare și reguli de dependență.Arhitectură la scară
  • 13Arhitectură hexagonală (ports & adapters)Domeniul fără Angular, porturi ca clase abstracte și adaptoare interschimbabile: HTTP, localStorage sau in-memory, fără să atingi UI-ul.Arhitectură la scară
  • 14Semnalele pe dinăuntruGraful reactiv: producători și consumatori, push-pull, de ce computed e lazy și glitch-free, problema diamantului și când rulează efectele.Reactivitate avansată
  • 14RxJS avansat: operatori de ordin superiorswitchMap, mergeMap, concatMap și exhaustMap pe o diagramă live, plus retry cu backoff, shareReplay și cum eviți scurgerile.Reactivitate avansată
  • 14NgRx SignalStoreState management la scară pe semnale: withState, withComputed, withMethods, rxMethod, entități și hooks — și când un store simplu e suficient.Reactivitate avansată
  • 14Mașini de stări cu signalsFără stări imposibile: o mașină tipizată pe signals, cu tranziții, guards și diagrama stărilor, live.Reactivitate avansată
  • 15WebSockets: timp real care rezistă rețeleiUn socket tipizat ca signals: reconectare cu backoff și jitter, heartbeat, coadă de mesaje cât ești offline și siguranță la SSR.Timp real și offline
  • 15Actualizări optimiste și rollbackInterfață instantanee, dar onestă: snapshot și rollback, răspunsuri sosite în altă ordine, chei de idempotență.Timp real și offline
  • 15Offline-first: Service Worker și cacheService Worker-ul Angular: strategii de cache, anunțarea versiunilor noi și o coadă pentru modificările făcute offline.Timp real și offline
  • 16Web WorkersCalcule grele fără să blochezi interfața: un worker real în pagină, comunicare prin mesaje și când nu merită.UI & performanță avansată
  • 16Liste uriașe: virtual scroll100.000 de rânduri fără lag: CDK virtual scroll, itemSize, trackBy și alternativele (paginare, infinite scroll).UI & performanță avansată
  • 16Animații moderne & View Transitionsanimate.enter și animate.leave în locul @angular/animations (depreciat), tranziții CSS și View Transitions la navigare.UI & performanță avansată
  • 16Memory leaks: cum le găsești și le repariSubscripții, timere și listeneri care trăiesc mai mult decât componenta lor — cum îi vezi în DevTools și cum îi închizi.UI & performanță avansată
  • 17Securitate: XSS, sanitizare & CSPCe face Angular automat împotriva XSS, când devine periculos bypassSecurityTrust, plus CSP cu nonce, Trusted Types și XSRF.Securitate & globalizare
  • 17Autentificare: token-uri & refreshUn interceptor care atașează token-ul, reîmprospătează o singură dată la 401 și reia cererile din coadă — plus unde ții token-ul.Securitate & globalizare
  • 17OAuth 2.1 și OpenID Connect cu PKCEFluxul authorization code cu PKCE, pas cu pas: verifier și challenge, state și nonce, ID token-ul și de ce un BFF e mai sigur.Securitate & globalizare
  • 17Internaționalizare (i18n)$localize, atributul i18n, ICU plural/select, build per limbă vs. traduceri la runtime și formatarea după locale.Securitate & globalizare