Angular 18 : signal inputs, outputs et model()

🏷️ Front-end 📅 12/04/2026 15:00:00 👤 Mezgani said
Angular Angular 18 Signals Input Output
Angular 18 : signal inputs, outputs et model()

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.

A retenir : Les fonctions 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" />
Note : 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" />
A retenir : 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.