O componentă comunică cu părintele prin input-uri (date care intră) și output-uri (evenimente care ies). Regula clasică rămâne: data down, events up. Ce s-a schimbat e API-ul: input() și output() sunt funcții, iar input-urile sunt semnale.
Exemplu complet
import { Component, booleanAttribute, computed, input, numberAttribute, output } from '@angular/core';
@Component({
selector: 'app-star-rating',
template: `
@for (star of stars(); track star) {
<button [class.on]="star <= value()" [disabled]="readonly()" (click)="rate.emit(star)">★</button>
}
`,
})
export class StarRating {
readonly value = input.required<number>(); // obligatoriu
readonly max = input(5, { transform: numberAttribute }); // max="10" → 10
readonly readonly = input(false, { transform: booleanAttribute }); // <app-star-rating readonly />
readonly rate = output<number>();
// input-urile sunt semnale → derivezi direct cu computed, fără ngOnChanges
protected readonly stars = computed(() => Array.from({ length: this.max() }, (_, i) => i + 1));
}- Dă o notă — părintele primește evenimentul (rate).
Opțiuni: required, transform, alias
| Scriere | Tip | Când |
|---|---|---|
input(5) | InputSignal<number> | Are o valoare implicită rezonabilă. |
input<string>() | InputSignal<string | undefined> | Cu adevărat opțional. |
input.required<User>() | InputSignal<User> | Componenta nu are sens fără el. |
transform: booleanAttribute | acceptă '', 'true' | Flag-uri: <app-x disabled />. |
transform: numberAttribute | acceptă '10' | Numere scrise ca atribute statice. |
// Alias (numele din template diferă de cel din clasă) — folosește rar
readonly label = input('', { alias: 'ariaLabel' });
// Transform custom: normalizezi valoarea la intrare
readonly tags = input([], {
transform: (value: string | string[]) => (Array.isArray(value) ? value : value.split(',')),
});
// Input opțional fără valoare implicită → tipul include undefined
readonly hint = input<string>(); // InputSignal<string | undefined>
// Output cu alias
readonly closed = output<void>({ alias: 'close' });
this.closed.emit();Înainte și acum
// ✓ API-ul modern: semnale + funcții
export class StarRating {
readonly value = input.required<number>();
readonly max = input(5, { transform: numberAttribute });
readonly rate = output<number>();
protected readonly stars = computed(() => Array.from({ length: this.max() }, (_, i) => i + 1));
}// ✗ API-ul vechi, bazat pe decoratori
export class StarRating implements OnChanges {
@Input({ required: true }) value!: number;
@Input({ transform: numberAttribute }) max = 5;
@Output() rate = new EventEmitter<number>();
stars: number[] = [];
ngOnChanges(changes: SimpleChanges) {
if (changes['max']) {
this.stars = Array.from({ length: this.max }, (_, i) => i + 1);
}
}
}Migrare automată: ng generate @angular/core:signal-input-migration și ng generate @angular/core:outputs.
Input-urile sunt read-only
export class Pager {
readonly page = input(1);
next() {
// ✗ input-urile sunt read-only în componentă
// this.page.set(this.page() + 1);
// ✓ variante:
// - emite un eveniment și lasă părintele să decidă: this.pageChange.emit(this.page() + 1)
// - folosește model() dacă e o valoare two-way → vezi pagina model()
// - folosește linkedSignal(() => this.page()) pentru o copie locală
}
}Abonare programatică la output-uri
// Te poți abona programatic la un output (ex. la o componentă creată dinamic)
const ref = viewContainer.createComponent(StarRating);
const sub = ref.instance.rate.subscribe((value) => console.log(value));
// sub.unsubscribe(); — se face automat când componenta e distrusă
// Sau mai simplu, cu bindings la creare (vezi „Componente dinamice”):
viewContainer.createComponent(StarRating, {
bindings: [inputBinding('value', this.rating), outputBinding('rate', (v) => this.save(v))],
});- Compilatorul aruncă eroare dacă legi de două ori același input/output pe un element.
- Atributele
data-*nu mai leagă input-uri sau output-uri (ex.data-valuenu mai ajunge în input-ulvalue). - v22.2: opțiunea
strictUnclaimedEventNamesprinde output-uri scrise greșit ((rated)în loc de(rate)).
Trei niveluri mai jos: prop drilling
Input-urile și output-urile leagă un părinte de copilul lui direct. Când datele stau sus și e nevoie de ele trei niveluri mai jos, fiecare componentă dintre ele trebuie să declare un input ca să le dea mai departe și un output ca să trimită evenimentele înapoi în sus — deși pe niciuna nu o interesează datele.
signal: countinput: countinput: countoutput: addedoutput: addedoutput: addedoutput: added- Input-uri declarate
- 2
- Output-uri declarate
- 4
- Componente care știu de coș
- 7
- Apasă Adaugă în coș.
Cu input-uri și output-uri, ProductCard, ProductGrid și ShopPage există doar ca să transmită evenimentul mai departe, iar fiecare e încă un loc în care se poate strica ceva când evenimentul își schimbă forma. Cu un store, doar cele două componente care chiar folosesc coșul știu de el. Păstrează input-urile și output-urile pentru relația directă — fac o componentă reutilizabilă și îi arată contractul — și folosește un serviciu când datele trebuie să treacă peste niveluri sau să ajungă la un frate.
// fiecare nivel dintre ei declară un output doar ca să dea click-ul mai departe
@Component({
selector: 'app-product-grid',
template: `<app-product-card (added)="added.emit()" />`,
})
export class ProductGrid {
readonly added = output();
}
// ProductCard, ShopPage: aceleași trei rânduri, câte o dată pentru fiecare
// …iar numărul coboară pe cealaltă ramură prin alte input-uri
@Component({
template: `
<app-header [count]="count()" />
<app-shop-page (added)="count.update((n) => n + 1)" />
`,
})
export class Shop {
protected readonly count = signal(0);
}Când DA, când NU
- Comunicare directă părinte ↔ copil.
- Componente dumb / UI reutilizabil (butoane, carduri, tabele).
- Evenimente cu semantică:
(save),(delete),(select).
- Valoare care trebuie sincronizată în ambele direcții →
model(). - Date pasate prin 3+ niveluri („prop drilling”) → un serviciu/store injectat.
- Comunicare între componente surori fără părinte comun util → serviciu.
- Configurare globală (temă, limbă) →
InjectionToken/ serviciu.
Capcane frecvente
- Citești
this.value()în constructor: input-urile nu sunt setate încă (input-urile required aruncă eroare). Folosește-le încomputed,effectsau template. - Obiecte mutate de copil: dacă primești un obiect prin input și îl modifici, modifici starea părintelui pe ascuns. Emite un eveniment în schimb.
- Output-uri numite ca evenimente DOM (
click,change) pot crea confuzii — evenimentul nativ face bubble și declanșează handler-ul de două ori. Alege nume specifice.
Fișă rapidă
input<T>() // InputSignal<T | undefined>
input(default) // InputSignal<T>
input.required<T>() // obligatoriu, eroare la compilare dacă lipsește
input(x, { transform, alias }) // booleanAttribute, numberAttribute, funcție proprie
output<T>() // OutputEmitterRef<T> → .emit(value)
output<T>({ alias })
// În template-ul părintelui
<app-x [value]="sig()" flag (changed)="onChanged($event)" />