Cours Angular · Module 6: Signals
Les signal inputs - comment un samouraï reçoit ses ordres
Dans cette leçon3
Le composant-carte d'un samouraï reçoit ses données du parent : le nom, le niveau, le clan. Autrefois il les recevait par @Input(), aujourd'hui par des signals. Cette leçon présente trois familles de ces entrées : le input() ordinaire (un ordre à sens unique), model() (une conversation à double sens) et les requêtes de signal vers les enfants. Toutes ont un point commun : tu les lis exactement comme n'importe quel autre signal, avec ().
input() - l'ordre venu d'en haut
input() transforme la donnée envoyée par le parent en un signal en lecture seule. Dans le template, tu le lis comme un signal ordinaire : name().
1import { Component, input } from '@angular/core';
2
3@Component({
4 selector: 'app-samurai-card',
5 template: `<h2>{{ name() }}</h2><p>Niveau : {{ level() }}</p>`
6})
7export class SamuraiCardComponent {
8 name = input('Unknown'); // optionnel, avec une valeur par defaut
9 level = input.required<number>(); // obligatoire
10 clan = input('Ronin', {
11 transform: (v: string) => v.toUpperCase() // met la valeur en majuscules
12 });
13}Ces trois variantes couvrent l'essentiel des besoins. input('Unknown') est un signal input optionnel : il porte une valeur par défaut, utilisée quand le parent ne transmet rien. input.required<number>() fait l'inverse - Angular lève une erreur si le parent oublie de le passer, ce qui te protège d'un oubli silencieux. Observe bien la forme : required n'est ni une fonction séparée comme requiredInput<string>(), ni un décorateur comme @RequiredInput(), c'est une propriété de input, d'où l'écriture input.required<string>(). L'ancien @Input({ required: true }) existe toujours, mais il te rend une valeur ordinaire, pas un signal. Enfin transform fait passer la valeur par une fonction dès l'entrée - ici chaque clan est stocké en majuscules, sans traitement manuel dans le composant - et l'option alias permet de renommer l'entrée côté parent. Le parent, lui, écrit comme toujours : <app-samurai-card name="Musashi" [level]="10">.
model() - la conversation à double sens
Parfois l'enfant ne doit pas seulement lire une valeur, mais aussi la modifier - comme une barre de vie. Trois directions existent : input() fait descendre la donnée du parent vers l'enfant, output() fait remonter un événement de l'enfant vers le parent (tu le déclares selected = output<string>() et tu le déclenches avec selected.emit(...)), et model() réunit les deux en une liaison bidirectionnelle : le parent fournit la valeur, l'enfant la met à jour, et le parent voit le changement.
1import { Component, model } from '@angular/core';
2
3@Component({
4 selector: 'app-health-bar',
5 template: `
6 <input type="range" [value]="health()"
7 (input)="health.set(+$any($event.target).value)">
8 `
9})
10export class HealthBarComponent {
11 health = model(100); // fonctionne comme [(ngModel)]
12}La différence avec input() tient en un mot, mais elle est décisive : un model() peut être assigné (health.set(...)), et la modification remonte jusqu'au parent. C'est pourquoi le parent l'attache dans les deux sens : <app-health-bar [(health)]="currentHealth">. Quand tu déplaces le curseur dans l'enfant, currentHealth change tout seul chez le parent. Retiens qu'aucune autre fonction ne fait cela : input() reste en lecture seule, output() ne transporte que des événements vers le haut, et computed() calcule une valeur dérivée que personne ne peut assigner. Seul model() prend en charge la syntaxe [(value)]. C'est l'équivalent, côté signals, de l'ancien [(ngModel)].
Les requêtes de signal vers les enfants
La dernière famille d'entrées ne vient pas du parent : ce sont des poignées vers tes propres enfants dans le template. Les anciens décorateurs @ViewChild et @ContentChild ont désormais leur version basée sur les signals.
1import { viewChild, viewChildren, contentChild } from '@angular/core';
2
3export class ParentComponent {
4 childComponent = viewChild(ChildComponent); // un seul enfant du propre template
5 allCards = viewChildren(CardComponent); // tous les enfants de ce type
6 projectedHeader = contentChild('header'); // un enfant projete via <ng-content>
7}Les noms disent où chercher : viewChild trouve un élément du template du composant lui-même, tandis que contentChild vise un élément projeté depuis l'extérieur par <ng-content>. Le pluriel (viewChildren, contentChildren) renvoie tous les éléments correspondants, et tu peux interroger soit une classe de composant, soit un nom de référence de template comme 'header'. Remarque que le vocabulaire n'a pas bougé : la version signal du décorateur @ViewChild s'appelle simplement viewChild(), avec une minuscule initiale - Angular n'a inventé ni querySignal(), ni childSignal(), ni selectChild(). Le gain est le même que partout ailleurs : le résultat est un signal, donc tu peux bâtir dessus un computed ou un effect au lieu d'attendre les hooks du cycle de vie.
Retiens de cette leçon leur point commun : toutes ces entrées sont des signals que tu lis avec (). input() reçoit l'ordre venu d'en haut, output() renvoie la réponse vers le haut, model() mène la conversation dans les deux sens, et viewChild/contentChild te donnent des poignées, elles aussi sous forme de signal, vers tes enfants.
Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Comment définis-tu un signal input requis ?
2. Quelle fonction crée une propriété qui prend en charge la liaison bidirectionnelle [(value)] dans un composant standalone ?
Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.
Tâches pratiques dans le jeu
- Tri horizontal
Ordonne, sur une seule ligne, la déclaration de la propriété en liaison bidirectionnelle health avec une valeur initiale de 100.
- Tri horizontal
Ordonne, sur une seule ligne, la déclaration du signal input optionnel name avec une valeur par défaut 'Unknown'.
- Éditeur de code
Complète la carte du samouraï : le nom doit venir du parent sous forme d'entrée signal (la nouvelle API à la place du décorateur @Input), et le choix revient au parent via output().
- Clique dans l'ordre
Ordonne l'appel qui crée un input requis de type number (la partie après le signe =).