À propos de ce cours
Une entrée déclarée avec input() est un signal en lecture seule que le parent remplit, par un attribut (du texte) ou par une liaison [x] (une expression). input.required() est vérifiée à la compilation. alias renomme l’entrée et transform convertit la valeur reçue. On réagit à une entrée avec computed, effect et linkedSignal. Les tableaux et les objets se transmettent par référence et ne doivent jamais être modifiés sur place. L’ancienne écriture @Input() est présentée avec sa migration. Le cours se termine par 5 activités corrigées et les erreurs fréquentes.
-
Objectifs
- Déclarer une entrée avec
input(): valeur par défaut, type explicite, entrée facultative ou obligatoire (input.required()) - Remplir une entrée depuis le parent, en distinguant attribut texte et liaison
[ ] - Renommer une entrée avec un alias et convertir la valeur reçue avec une transformation (
numberAttribute,booleanAttribute, fonction personnalisée) - Réagir à une entrée :
computed()pour une valeur,effect()pour une action,linkedSignal()pour un état local - Transmettre des tableaux et des objets sans piège, en respectant l'immutabilité
- Lire et migrer l'ancienne écriture
@Input()+ngOnChanges
- Déclarer une entrée avec
-
Présentation
- Dans « La communication entre composants Angular », vous avez vu les six moyens de faire dialoguer des composants. Le plus utilisé, de loin, est l'entrée: c'est par elle qu'un composant reçoit les données qu'il affiche.
- Un composant avec des entrées est réutilisable: une même carte affiche n'importe quel produit, une même jauge n'importe quelle valeur. Le parent décide quoi afficher, l'enfant décide comment.
- Depuis Angular 17, on déclare une entrée avec la fonction
input(), qui renvoie un signal en lecture seule. Ce chapitre en explore toutes les possibilités.

- Pour suivre ce chapitre, continuez dans le projet
premiers-pas-angularet créez un dossiersrc/app/entrees/. Tous les exemples ont été testés avec Angular 22 (composantsOnPushpar défaut, sans Zone.js).
-
Déclarer et remplir une entrée
- Dans l'enfant, une entrée est une propriété initialisée avec
input(). On l'importe depuis@angular/coreet on la déclarereadonly: la propriété elle-même ne sera jamais réaffectée. - Dans le template de l'enfant, on la lit comme tout signal :
{{ nom() }}. - Dans le parent, on la remplit sur la balise de l'enfant, avec un attribut (texte fixe) ou une liaison de propriété
[nom]="…"(expression).
- Dans l'enfant, une entrée est une propriété initialisée avec
import { Component, input, signal } from '@angular/core';
// Composant enfant : trois entrées, trois façons de les déclarer
@Component({
selector: 'app-badge-utilisateur',
template: `
<span class="badge">{{ nom() }}</span>
<small>rôle : {{ role() ?? 'non précisé' }} · âge : {{ age() }} ({{ typeAge() }})</small>
`,
styles: `.badge { background: #3178c6; color: #fff; padding: 2px 10px; border-radius: 12px; margin-right: 8px; } small { color: #475569; }
:host { display: block; margin: 6px 0; }`,
})
export class BadgeUtilisateur {
readonly nom = input('Invité'); // valeur par défaut : le type string est déduit
readonly role = input<string>(); // pas de valeur par défaut : string | undefined
readonly age = input<number | string>(0);
protected typeAge(): string {
return typeof this.age();
}
}
// Composant parent : il remplit les entrées depuis son template
@Component({
selector: 'app-premiere-entree',
imports: [BadgeUtilisateur],
template: `
<app-badge-utilisateur />
<app-badge-utilisateur nom="Amira" role="formatrice" age="34" />
<app-badge-utilisateur [nom]="prenom()" [age]="34" />
<button (click)="prenom.set('Sami')">Changer le prénom</button>
`,
})
export class PremiereEntree {
protected readonly prenom = signal('Ines');
}
Invité rôle : non précisé · âge : 0 (number)
Amira rôle : formatrice · âge : 34 (string)
Ines rôle : non précisé · âge : 34 (number)
(Changer le prénom) Sami rôle : non précisé · âge : 34 (number)
input('Invité'): valeur par défaut, utilisée quand le parent ne fournit rien ; TypeScript en déduit le typestring.input<string>(): pas de valeur par défaut, le type devientstring | undefined. D'oùrole() ?? 'non précisé'. Écrirethis.role().toUpperCase()est refusé :TS2532: Object is possibly 'undefined'.- Le deuxième badge reçoit
age="34": un attribut donne toujours du texte (string). Le troisième reçoit[age]="34": la liaison évalue l'expression, c'est unnumber. [nom]="prenom()"relie l'entrée au signal du parent : quand il change, l'enfant se met à jour.

- Attention :
nom="prenom()"sans crochets n'appelle rien : l'enfant reçoit le texte « prenom() ». Et une liaison au mauvais type est refusée :[nom]="34"sur une entréestringdonneTS2322: Type 'number' is not assignable to type 'string'.
-
Une entrée obligatoire :
input.required()- Certains composants n'ont aucun sens sans leurs données : une carte produit sans produit, une fiche sans contact. Plutôt qu'une valeur par défaut artificielle, on déclare l'entrée obligatoire.
input.required<Produit>()n'a pas de valeur par défaut et son type n'inclut pasundefined. C'est le compilateur qui vérifie que le parent la fournit.
import { Component, computed, input } from '@angular/core';
import { DecimalPipe } from '@angular/common';
export interface Produit {
id: number;
nom: string;
prixHT: number;
stock: number;
}
// Composant enfant : il ne peut pas fonctionner sans produit
@Component({
selector: 'app-carte-produit',
imports: [DecimalPipe],
template: `
<strong>{{ produit().nom }}</strong>
<span>{{ prixTTC() | number: '1.2-2' }} DT TTC</span>
<span [class]="etat().classe">{{ etat().texte }}</span>
`,
styles: `:host { display: grid; grid-template-columns: 1fr 130px 110px; padding: 6px 4px; border-bottom: 1px solid #e2e8f0; }
.ok { color: #15803d; } .bas { color: #b45309; } .rupture { color: #dc2626; }`,
})
export class CarteProduit {
readonly produit = input.required<Produit>();
readonly tva = input(19); // en %
// Les valeurs dérivées d'une entrée sont des computed
protected readonly prixTTC = computed(() => this.produit().prixHT * (1 + this.tva() / 100));
protected readonly etat = computed(() => {
const s = this.produit().stock;
if (s === 0) return { texte: 'Rupture', classe: 'rupture' };
return s < 5 ? { texte: `Plus que ${s}`, classe: 'bas' } : { texte: 'En stock', classe: 'ok' };
});
}
@Component({
selector: 'app-entree-requise',
imports: [CarteProduit],
template: `
@for (p of produits; track p.id) {
<app-carte-produit [produit]="p" />
}
<app-carte-produit [produit]="produits[0]" [tva]="7" />
`,
})
export class EntreeRequise {
protected readonly produits: Produit[] = [
{ id: 1, nom: 'Clavier', prixHT: 45, stock: 12 },
{ id: 2, nom: 'Souris', prixHT: 20, stock: 3 },
{ id: 3, nom: 'Écran 24"', prixHT: 390, stock: 0 },
];
}
Clavier 53,55 DT TTC En stock
Souris 23,80 DT TTC Plus que 3
Écran 24" 464,10 DT TTC Rupture
Clavier 48,15 DT TTC En stock (tva = 7)
- Oublier
[produit]:NG8008: Required input 'produit' from component CarteProduit must be specified. - Fournir un objet incomplet :
TS2739: Type '{ nom: string; }' is missing the following properties from type 'Produit': id, prixHT, stock. Le template du parent est vérifié comme du TypeScript. prixTTCetetatsont descomputed: toute valeur déduite d'une entrée se calcule ainsi, et se met à jour avec elle.- Attention : une entrée obligatoire n'a pas encore de valeur dans le constructeur.
console.log(this.produit())dans le constructeur est refusé :NG8118: `produit` is a required `input` and does not have a value in this context.

-
Alias et transformations
- Un alias donne à l'entrée un nom public différent de son nom interne :
input('', { alias: 'libelle' }). Le parent écritlibelle="…", la classe utilisetitre(). À réserver aux cas utiles (nom public imposé, conflit de noms) : en général, un seul nom est plus clair. - Une transformation est une fonction appliquée à chaque valeur reçue, avant que l'enfant ne la lise. Angular en fournit deux :
numberAttribute(texte → nombre) etbooleanAttribute(attribut présent →true). On peut aussi écrire la sienne.
- Un alias donne à l'entrée un nom public différent de son nom interne :
import { Component, booleanAttribute, computed, input, numberAttribute, signal } from '@angular/core';
// Une fonction de transformation : un nombre ramené entre 0 et 100
function pourcentage(v: string | number): number {
return Math.min(100, Math.max(0, numberAttribute(v, 0)));
}
@Component({
selector: 'app-jauge',
template: `
<span class="etiquette">{{ etiquette() }}</span>
<span class="fond" [class.compacte]="compacte()"><span class="barre" [style.width.%]="valeur()"></span></span>
<span>{{ valeur() }} %</span>
`,
styles: `:host { display: grid; grid-template-columns: 110px 1fr 50px; gap: 8px; align-items: center; margin: 6px 0; }
.fond { background: #e2e8f0; border-radius: 8px; height: 16px; overflow: hidden; } .fond.compacte { height: 6px; }
.barre { display: block; height: 100%; background: #3178c6; } .etiquette { font-weight: 600; }`,
})
export class Jauge {
readonly valeur = input(0, { transform: pourcentage }); // "150" → 100
readonly compacte = input(false, { transform: booleanAttribute }); // attribut seul → true
readonly titre = input('', { alias: 'libelle' }); // nom public : libelle
protected readonly etiquette = computed(() => this.titre().trim().toUpperCase() || 'SANS NOM');
}
@Component({
selector: 'app-alias-transform',
imports: [Jauge],
template: `
<app-jauge libelle=" html " valeur="80" />
<app-jauge libelle="css" valeur="150" compacte />
<app-jauge [valeur]="-20" />
<app-jauge libelle="angular" [valeur]="niveau()" />
<input type="range" min="0" max="100" [value]="niveau()" (input)="niveau.set($any($event.target).value)" />
`,
})
export class AliasTransform {
protected readonly niveau = signal<string | number>(45);
}
HTML ████████░░ 80 %
CSS ██████████ 100 % (barre fine : compacte)
SANS NOM ░░░░░░░░░░ 0 %
ANGULAR ████░░░░░░ 45 %
(curseur déplacé à 70) ANGULAR 70 %

valeur="150": le texte « 150 » est converti en nombre, puis ramené à 100 parpourcentage;[valeur]="-20"devient 0.- L'attribut seul
compactevaut la chaîne vide"":booleanAttributela transforme entrue. Sans transformation, il faudrait écrire[compacte]="true". - Le type accepté par l'entrée est celui du paramètre de la transformation (
string | number) :[valeur]="true"est refusé (TS2322: Type 'boolean' is not assignable to type 'string | number'.). - Avec un alias, l'ancien nom n'existe plus côté parent :
[titre]="'x'"donneNG8002: Can't bind to 'titre' since it isn't a known property of 'app-jauge'. - Une transformation doit rester pure: rapide, sans effet de bord. Pour une valeur qui dépend de plusieurs entrées (une valeur bornée par un
max), utilisez plutôt uncomputed(activité 2).
-
Réagir au changement d'une entrée
- Une entrée étant un signal, tous les outils réactifs vus dans « Les signals dans Angular » s'appliquent :
computed()pour une valeur déduite (prix TTC, initiales, pourcentage) ;effect()pour une action à mener à chaque changement (charger des données, écrire un journal) ;linkedSignal()pour un état local, modifiable par l'enfant, qui doit repartir de zéro quand l'entrée change.
- Une entrée étant un signal, tous les outils réactifs vus dans « Les signals dans Angular » s'appliquent :
import { Component, computed, effect, input, linkedSignal, signal } from '@angular/core';
const METEO: Record<string, string> = { Tunis: '24 °C, ensoleillé', Sousse: '22 °C, nuageux', Sfax: '25 °C, venteux' };
// 1) Réagir à une entrée avec un effect : simuler un chargement
@Component({
selector: 'app-bulletin-meteo',
template: `<p>{{ ville() }} : {{ chargement() ? 'chargement…' : resultat() }}</p>`,
})
export class BulletinMeteo {
readonly ville = input.required<string>();
protected readonly chargement = signal(false);
protected readonly resultat = signal('');
constructor() {
effect((onCleanup) => {
const v = this.ville(); // l'effet dépend de l'entrée
this.chargement.set(true);
const t = setTimeout(() => {
this.resultat.set(METEO[v] ?? 'inconnue');
this.chargement.set(false);
}, 600);
onCleanup(() => clearTimeout(t)); // annule un chargement devenu inutile
});
}
}
// 2) Un état local qui se réinitialise quand l'entrée change : linkedSignal
@Component({
selector: 'app-liste-paginee',
template: `
<ul>@for (e of visibles(); track e) { <li>{{ e }}</li> }</ul>
<button (click)="page.update((p) => p - 1)" [disabled]="page() === 0">‹</button>
page {{ page() + 1 }} / {{ nbPages() }}
<button (click)="page.update((p) => p + 1)" [disabled]="page() >= nbPages() - 1">›</button>
`,
})
export class ListePaginee {
readonly elements = input.required<string[]>();
readonly parPage = input(2);
readonly page = linkedSignal({ source: this.elements, computation: () => 0 }); // revient à 0 si la liste change
protected readonly nbPages = computed(() => Math.max(1, Math.ceil(this.elements().length / this.parPage())));
protected readonly visibles = computed(() => this.elements().slice(this.page() * this.parPage(), (this.page() + 1) * this.parPage()));
}
@Component({
selector: 'app-reagir-entree',
imports: [BulletinMeteo, ListePaginee],
template: `
@for (v of villes; track v) { <button (click)="ville.set(v)">{{ v }}</button> }
<app-bulletin-meteo [ville]="ville()" />
<hr />
<button (click)="liste.set(langages)">Langages</button>
<button (click)="liste.set(outils)">Outils</button>
<app-liste-paginee [elements]="liste()" />
`,
})
export class ReagirEntree {
protected readonly villes = ['Tunis', 'Sousse', 'Sfax'];
protected readonly ville = signal('Tunis');
protected readonly langages = ['TypeScript', 'HTML', 'CSS', 'SCSS', 'JavaScript'];
protected readonly outils = ['VS Code', 'Git', 'Angular CLI'];
protected readonly liste = signal(this.langages);
}
Tunis : chargement… puis Tunis : 24 °C, ensoleillé
(Sousse puis Sfax, très vite) Sfax : chargement… puis Sfax : 25 °C, venteux (Sousse n'est jamais affiché)
TypeScript, HTML page 1 / 3 (› ›) JavaScript page 3 / 3
(Outils) VS Code, Git page 1 / 2 ← la page est revenue à 1

- L'effet lit
this.ville(): il est relancé à chaque nouvelle ville.onCleanupannule le chargement précédent, sinon une réponse lente (Sousse) pourrait arriver après la plus récente (Sfax) et l'écraser. linkedSignal({ source: this.elements, computation: () => 0 }):pageest un signal modifiable par les boutons, mais il revient à 0 chaque fois que la source (la liste reçue) change.- Ces trois outils remplacent le crochet de cycle de vie
ngOnChangesde l'ancienne écriture.
-
Transmettre des tableaux et des objets
- Une entrée peut recevoir n'importe quel type : tableau, objet, classe. Mais Angular ne détecte un changement que si la référence change : un tableau modifié sur place reste « le même » tableau.
import { Component, computed, input, signal } from '@angular/core';
@Component({
selector: 'app-resume-notes',
template: `<p>{{ notes().length }} note(s) · moyenne {{ moyenne() }}</p>`,
})
export class ResumeNotes {
readonly notes = input.required<number[]>();
protected readonly moyenne = computed(() => {
const n = this.notes();
return n.length ? (n.reduce((a, b) => a + b, 0) / n.length).toFixed(2) : '–';
});
}
@Component({
selector: 'app-entrees-objets',
imports: [ResumeNotes],
template: `
<app-resume-notes [notes]="notes()" />
<button (click)="ajouterMauvais()">push() : modifier sur place</button>
<button (click)="ajouterBon()">update() : nouveau tableau</button>
`,
})
export class EntreesObjets {
protected readonly notes = signal([12, 15]);
protected ajouterMauvais(): void {
this.notes().push(20); // même tableau : aucun signal ne change, rien ne se met à jour
}
protected ajouterBon(): void {
this.notes.update((n) => [...n, 8]); // nouveau tableau : l'enfant recalcule
}
}
2 note(s) · moyenne 13.50
(push() : modifier sur place) 2 note(s) · moyenne 13.50 ← rien ne bouge
(update() : nouveau tableau) 4 note(s) · moyenne 13.75 ← le 20 « caché » réapparaît avec le 8

push()modifie le tableau existant : le signal du parent n'est pas prévenu, l'entrée de l'enfant non plus. Pire : la valeur est ajoutée « en douce » et apparaît plus tard, au prochain vrai changement. L'affichage devient incohérent.- La règle est la même dans les deux sens : le parent crée un nouveau tableau (
[...n, 8],filter,map), et l'enfant ne modifie jamais un objet reçu (voir la copielinkedSignaldans « La communication entre composants Angular »).
-
L'ancienne écriture :
@Input()- Avant les signals, une entrée était une propriété ordinaire décorée par
@Input()(voir « Les décorateurs Angular »). Vous la rencontrerez dans la plupart des projets et des tutoriels existants. - Les mêmes options existent :
required,alias(premier argument texte) ettransform. Les valeurs dérivées se calculent dans le crochetngOnChanges, appelé à chaque changement d'entrée.
- Avant les signals, une entrée était une propriété ordinaire décorée par
import { Component, Input, OnChanges, SimpleChanges, booleanAttribute, numberAttribute, signal } from '@angular/core';
// Avant Angular 17 : le décorateur @Input() sur une propriété ordinaire
@Component({
selector: 'app-note-ancienne',
template: `<p>{{ texte }} : {{ note }}/20 {{ mention }} @if (provisoire) { (provisoire) }</p>`,
})
export class NoteAncienne implements OnChanges {
@Input({ required: true }) matiere!: string;
@Input({ transform: numberAttribute }) note = 0;
@Input({ transform: booleanAttribute }) provisoire = false;
@Input('libelle') texte = 'Note';
mention = '';
// Les valeurs dérivées sont recalculées à la main à chaque changement
ngOnChanges(changes: SimpleChanges): void {
if (changes['note']) {
this.mention = this.note >= 16 ? '★ Très bien' : this.note >= 10 ? '✓' : '✗';
}
}
}
@Component({
selector: 'app-ancienne-ecriture',
imports: [NoteAncienne],
template: `
<app-note-ancienne matiere="Angular" libelle="Angular" [note]="note()" />
<app-note-ancienne matiere="UML" note="9" provisoire />
<button (click)="note.update((n) => n + 2)">+2</button>
`,
})
export class AncienneEcriture {
protected readonly note = signal(14);
}
Angular : 14/20 ✓
Note : 9/20 ✗ (provisoire)
(+2) Angular : 16/20 ★ Très bien

- La propriété se lit sans parenthèses (
{{ note }}) et le!dematiere!: stringindique à TypeScript qu'elle sera remplie plus tard. mentiondoit être recalculée à la main dansngOnChanges; avecinput(), uncomputeds'en charge sans risque d'oubli.- Rien n'empêche d'écrire
this.note = 0dans l'enfant, ce qui désynchronise le parent ; avecinput(), c'est une erreur de compilation. - Angular fournit une migration automatique :
ng generate @angular/core:signal-input-migration.
-
Aide-mémoire

-
Activité 1 : une carte de profil réutilisable
- Créer le composant
app-carte-profilavec les entrées suivantes :nom: obligatoire ;poste: par défaut « Membre de l'équipe » ;photo: facultative, sans valeur par défaut ; si elle est absente, afficher les initiales (2 lettres majuscules, calculées par uncomputed) ;couleur: couleur de fond de l'avatar, par défaut#3178c6;enLigne: booléen avecbooleanAttribute, qui affiche une pastille verte.
- Le parent
app-equipeaffiche trois cartes écrites à la main (attributs seulement, dontenLigneseul) et une carte par stagiaire avec@foret[nom].
- Créer le composant

- Pourquoi « ines gharbi » affiche-t-il « IG » et « Karim » seulement « K » ?
- Que se passe-t-il si l'on retire
nomd'une carte ? Et si l'on écritenLigne="false"?
-
Activité 2 : une barre de progression configurable
- Créer
app-barre-progressionavec quatre entrées :max(100 par défaut), transformée pour être un nombre au moins égal à 1 ;valeur, avecnumberAttribute, déclarée sous le nom internevaleurBrute(alias) ;libelle(nom internetitre) etrayee(booleanAttribute).
- Trois
computed: la valeur bornée entre 0 etmax, le pourcentage arrondi, et le niveau (faibleen rouge sous 34 %,moyenen orange sous 67 %,bonen vert). - Le parent affiche quatre barres, dont « Valeur trop grande » (
valeur="150") et « TP 2 », liée à un signal modifié par deux boutons −1 / +1.
- Créer

- Vérifier : 9 / 12 donne 75 % en vert, 2 / 10 donne 20 % en rouge, et « TP 2 » ne dépasse jamais 10 / 10.
- Pourquoi la valeur bornée est-elle un
computedet non une transformation ?
-
Activité 3 : une liste paginée
- Créer
app-pagination-liste: entrée obligatoireelements(tableau de textes), entréeparPage(4 par défaut,numberAttribute), et un signalpagecréé aveclinkedSignalqui revient à la première page quand la liste change. - Afficher les éléments de la page dans une liste numérotée (
[start]), « Aucun résultat. » si la liste est vide, et quatre boutons « ‹ › » (première, précédente, suivante, dernière page) désactivés aux extrémités, avec « Page 2 / 3 ». - Le parent
app-annuaire-paginecontient 16 villes de Tunisie et un champ de recherche ; il transmet la liste filtrée (uncomputed, donc un nouveau tableau à chaque saisie).
- Créer

- Aller à la page 4, puis taper « a » : pourquoi revient-on à la page 1 / 3 ?
- Que se passerait-il avec un simple
signal(0)à la place dulinkedSignal?
-
Activité 4 : une fiche météo qui se recharge
- Créer
app-fiche-meteoavec une entrée obligatoireville. Une fonction fourniechargerMeteo(ville, rappel)simule un appel réseau de 800 ms et renvoie une fonction d'annulation. - Dans le constructeur, un
effect:- passe l'état à
chargementà chaque nouvelle ville ; - appelle
chargerMeteopuis affiche la température, le ciel et le vent, ou « Ville inconnue. » ; - annule la requête précédente avec
onCleanup; - incrémente un signal public
chargementsà chaque réponse appliquée.
- passe l'état à
- Le parent propose quatre boutons (Tunis, Sousse, Tozeur, Atlantis) et affiche « Réponses appliquées : n » grâce à une variable de référence
#fiche.
- Créer

- Cliquer très vite sur Sousse, Tozeur, Tunis, Sousse : combien de réponses sont appliquées ? Recommencer sans
onCleanupet comparer. - Pourquoi utiliser un
effectici, et pas uncomputed?
-
Activité 5 : migrer un composant
@Input()versinput()- On vous fournit le composant
app-prix-ancienci-dessous, écrit avec l'ancienne syntaxe :
- On vous fournit le composant
@Component({
selector: 'app-prix-ancien',
template: `<span [class.promo]="promo">{{ affiche }}</span>`,
})
export class PrixAncien implements OnChanges {
@Input({ required: true, transform: numberAttribute }) montant!: number;
@Input() devise = 'DT';
@Input({ transform: numberAttribute }) remise = 0;
@Input({ transform: booleanAttribute }) promo = false;
affiche = '';
ngOnChanges(): void {
const net = this.montant * (1 - this.remise / 100);
this.affiche = `${net.toFixed(2)} ${this.devise}` + (this.remise ? ` (−${this.remise} %)` : '');
}
}
- Écrire
app-prix, son équivalent avecinput(),input.required()et uncomputedà la place dengOnChanges. - Afficher les deux versions côte à côte dans un tableau (livre 89,90 DT ; casque 129 DT avec 20 % de remise en promotion ; cours 150 € dont la remise est modifiée par un bouton de 0 à 30 %) : les deux colonnes doivent toujours être identiques.

- Combien de lignes avez-vous supprimées ? Quel risque d'oubli disparaît avec le
computed? - Lancer
ng generate @angular/core:signal-input-migrationsur une copie dePrixAncienet comparer avec votre version.
- Les corrigés des cinq activités, testés avec Angular 22, sont fournis dans l'archive de la leçon (dossier
corriges/).
-
Erreurs fréquentes
| Message ou symptôme | Cause | Solution |
|---|---|---|
NG8008: Required input 'produit' from component CarteProduit must be specified. |
Entrée obligatoire non fournie | Ajouter [produit]="…" sur la balise |
NG8002: Can't bind to 'titre' since it isn't a known property of 'app-jauge'. |
Entrée inexistante, faute de frappe, nom interne au lieu de l'alias, ou composant non importé | Utiliser le nom public ; vérifier imports |
TS2339: Property 'set' does not exist on type 'InputSignal<string>'. |
Tentative de modifier une entrée dans l'enfant | linkedSignal, model() ou une sortie |
TS2532: Object is possibly 'undefined'. |
Entrée input<T>() sans valeur par défaut |
Valeur par défaut, ??, ?. ou input.required |
TS2322: Type 'number' is not assignable to type 'string'. |
Liaison d'une valeur du mauvais type | Corriger l'expression ou le type de l'entrée |
TS2322: Type 'boolean' is not assignable to type 'string | number'. |
Valeur refusée par le paramètre de la transformation | Passer une valeur du type attendu |
TS2739: Type '{ nom: string; }' is missing the following properties … |
Objet incomplet transmis | Fournir toutes les propriétés de l'interface |
NG8118: `produit` is a required `input` and does not have a value in this context. |
Entrée obligatoire lue dans le constructeur | La lire dans un computed, un effect ou le template |
| L'enfant reçoit « prenom() » au lieu de la valeur | Crochets oubliés : nom="prenom()" |
[nom]="prenom()" |
age="34" donne une chaîne, les calculs échouent |
Un attribut transmet toujours du texte | [age]="34" ou numberAttribute |
| L'enfant ne voit pas un élément ajouté | Tableau modifié sur place (push) |
update((l) => [...l, x]) |
| Un ancien résultat écrase le plus récent | Effet sans onCleanup |
Annuler l'action précédente dans onCleanup |
-
À retenir
input()déclare une entrée : un signal en lecture seule, rempli par le parent, lu avecnom().input(valeur)fixe une valeur par défaut ;input<T>()peut valoirundefined;input.required<T>()est vérifiée à la compilation.- Un attribut transmet du texte ; une liaison
[x]="…"transmet le résultat d'une expression, réactif s'il s'agit d'un signal. aliasrenomme l'entrée côté parent ;transformconvertit la valeur reçue (numberAttribute,booleanAttribute, fonction pure).computedpour une valeur déduite,effect(aveconCleanup) pour une action,linkedSignalpour un état local réinitialisé.- Tableaux et objets se transmettent par référence : on crée toujours une nouvelle copie au lieu de modifier sur place.
@Input()+ngOnChangesest l'ancienne écriture : à connaître, à migrer versinput().
Les entrées sont le contrat d'un composant : bien typées, avec des valeurs par défaut sensées, elles rendent le composant facile à réutiliser et impossible à mal utiliser.