Maîtrisez les nouvelles API Angular 18 : input(), output() et model() pour créer des composants réactifs sans décorateurs, plus simples et plus performants.
Contexte : les signaux dans Angular
Angular 16 a introduit les Signals comme mécanisme de réactivité fine. Angular 18 franchit une étape majeure en permettant de définir les inputs et outputs des composants sous forme de signaux, sans décorateurs @Input() et @Output().
Cette évolution simplifie la communication entre composants et s'intègre nativement avec la détection de changements basée sur les signaux.
input(), output() et model() ne sont pas des décorateurs — ce sont des fonctions du framework. Elles produisent des signaux, ce qui les rend compatibles avec computed() et effect().
signal input() — remplacer @Input
La fonction input() remplace le décorateur @Input() et retourne un signal en lecture seule.
import { Component, input, computed } from '@angular/core';
@Component({
selector: 'app-user-card',
standalone: true,
template: `
<div>
<h3>{{ fullName() }}</h3>
<p>{{ user().email }}</p>
</div>
`
})
export class UserCardComponent {
// Input requis
user = input.required<User>();
// Input optionnel avec valeur par défaut
showAvatar = input(true);
// Computed dérivé de l'input signal
fullName = computed(() => `${this.user().firstName} ${this.user().lastName}`);
}
Utilisation dans le parent :
<app-user-card [user]="currentUser" [showAvatar]="false" />
input.required() génère une erreur à la compilation si l'input n'est pas fourni par le parent. Fini les vérifications manuelles avec !.
signal output() — remplacer @Output
La fonction output() remplace @Output() EventEmitter. Elle retourne un objet avec une méthode .emit().
import { Component, output } from '@angular/core';
@Component({
selector: 'app-search-bar',
standalone: true,
template: `
<input
[value]="value"
(input)="onInput($event)"
(keydown.enter)="onSubmit()"
/>
`
})
export class SearchBarComponent {
// Ancien style
// @Output() searched = new EventEmitter<string>();
// Nouveau style
searched = output<string>();
value = '';
onInput(event: Event) {
this.value = (event.target as HTMLInputElement).value;
}
onSubmit() {
this.searched.emit(this.value);
}
}
Le parent écoute l'événement de la même façon qu'avant :
<app-search-bar (searched)="handleSearch($event)" />
model() — liaison bidirectionnelle
model() est la nouveauté la plus marquante : il combine un input et un output en un seul signal accessible en lecture et en écriture. Parfait pour les composants de formulaire.
import { Component, model } from '@angular/core';
@Component({
selector: 'app-toggle',
standalone: true,
template: `
<button (click)="toggle()">
{{ checked() ? 'ON' : 'OFF' }}
</button>
`
})
export class ToggleComponent {
checked = model(false); // valeur par défaut: false
toggle() {
this.checked.set(!this.checked()); // met à jour ET émet automatiquement
}
}
Utilisation avec two-way binding dans le parent :
// Template parent
<app-toggle [(checked)]="isActive" />
// Ou sans two-way binding
<app-toggle [checked]="isActive" (checkedChange)="isActive = $event" />
model() génère automatiquement un output nommé checkedChange (nom de la propriété + "Change"). Le two-way binding [(checked)] fonctionne nativement.
Comparaison avant / après
// ===== AVANT Angular 18 =====
@Component({ ... })
export class OldComponent {
@Input() title: string = '';
@Input() required count!: number;
@Output() titleChange = new EventEmitter<string>();
}
// ===== APRÈS Angular 18 =====
@Component({ ... })
export class NewComponent {
title = model(''); // two-way bindable
count = input.required<number>(); // required, type-safe
}
Migration progressive
Les deux syntaxes coexistent. Vous pouvez migrer progressivement composant par composant. Angular fournit également un schematic de migration :
ng generate @angular/core:signal-input-migration
Ce schematic convertit automatiquement les @Input() en input() dans les composants standalone.