Avez-vous une question? (216) 97 656 803 hajjriadh@gmail.com
Angular

Transmettre des donnees avec input

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 compilatio...

0 chapitres 0 QCM Accès gratuit
Dans cet article

À 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.
  1. 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
  1. 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.

Une entrée transmet une valeur du parent vers l'enfant

  • Pour suivre ce chapitre, continuez dans le projet premiers-pas-angular et créez un dossier src/app/entrees/. Tous les exemples ont été testés avec Angular 22 (composants OnPush par défaut, sans Zone.js).
  1. 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/core et on la déclare readonly: 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).
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 type string.
  • input<string>(): pas de valeur par défaut, le type devient string | undefined. D'où role() ?? 'non précisé'. Écrire this.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 un number.
  • [nom]="prenom()" relie l'entrée au signal du parent : quand il change, l'enfant se met à jour.

Attribut ou liaison : ce que reçoit l'enfant

  • 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ée string donne TS2322: Type 'number' is not assignable to type 'string'.
  1. 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 pas undefined. 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.
  • prixTTC et etat sont des computed: 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.

Les options de déclaration de input()

  1. Alias et transformations

    • Un alias donne à l'entrée un nom public différent de son nom interne : input('', { alias: 'libelle' }). Le parent écrit libelle="…", la classe utilise titre(). À 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) et booleanAttribute (attribut présent → true). On peut aussi écrire la sienne.
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 %

Les transformations d'entrée

  • valeur="150": le texte « 150 » est converti en nombre, puis ramené à 100 par pourcentage; [valeur]="-20" devient 0.
  • L'attribut seul compacte vaut la chaîne vide "": booleanAttribute la transforme en true. 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'" donne NG8002: 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 un computed (activité 2).
  1. 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.
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

Réagir à une entrée : template, computed, effect et linkedSignal

  • L'effet lit this.ville(): il est relancé à chaque nouvelle ville. onCleanup annule 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 }): page est 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 ngOnChanges de l'ancienne écriture.
  1. 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

Ne jamais modifier sur place un tableau transmis en entrée

  • 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 copie linkedSignal dans « La communication entre composants Angular »).
  1. 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) et transform. Les valeurs dérivées se calculent dans le crochet ngOnChanges, appelé à chaque changement d'entrée.
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

Correspondance entre @Input() et input()

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

Aide-mémoire de input() dans Angular

  1. Activité 1 : une carte de profil réutilisable

    • Créer le composant app-carte-profil avec 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 un computed) ;
      • couleur: couleur de fond de l'avatar, par défaut #3178c6;
      • enLigne: booléen avec booleanAttribute, qui affiche une pastille verte.
    • Le parent app-equipe affiche trois cartes écrites à la main (attributs seulement, dont enLigne seul) et une carte par stagiaire avec @for et [nom].

Résultat attendu de l'activité 1 : les cartes de l'équipe

  • Pourquoi « ines gharbi » affiche-t-il « IG » et « Karim » seulement « K » ?
  • Que se passe-t-il si l'on retire nom d'une carte ? Et si l'on écrit enLigne="false"?
  1. Activité 2 : une barre de progression configurable

    • Créer app-barre-progression avec quatre entrées :
      • max (100 par défaut), transformée pour être un nombre au moins égal à 1 ;
      • valeur, avec numberAttribute, déclarée sous le nom interne valeurBrute (alias) ;
      • libelle (nom interne titre) et rayee (booleanAttribute).
    • Trois computed: la valeur bornée entre 0 et max, le pourcentage arrondi, et le niveau (faible en rouge sous 34 %, moyen en orange sous 67 %, bon en 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.

Résultat attendu de l'activité 2 : les barres de progression

  • 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 computed et non une transformation ?
  1. Activité 3 : une liste paginée

    • Créer app-pagination-liste: entrée obligatoire elements (tableau de textes), entrée parPage (4 par défaut, numberAttribute), et un signal page créé avec linkedSignal qui 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-pagine contient 16 villes de Tunisie et un champ de recherche ; il transmet la liste filtrée (un computed, donc un nouveau tableau à chaque saisie).

Résultat attendu de l'activité 3 : la liste paginée filtrée

  • 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 du linkedSignal?
  1. Activité 4 : une fiche météo qui se recharge

    • Créer app-fiche-meteo avec une entrée obligatoire ville. Une fonction fournie chargerMeteo(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 chargerMeteo puis 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.
    • 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.

Résultat attendu de l'activité 4 : la fiche météo

  • Cliquer très vite sur Sousse, Tozeur, Tunis, Sousse : combien de réponses sont appliquées ? Recommencer sans onCleanup et comparer.
  • Pourquoi utiliser un effect ici, et pas un computed?
  1. Activité 5 : migrer un composant @Input() vers input()

    • On vous fournit le composant app-prix-ancien ci-dessous, écrit avec l'ancienne syntaxe :
@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 avec input(), input.required() et un computed à la place de ngOnChanges.
  • 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.

Résultat attendu de l'activité 5 : avant et après la migration

  • Combien de lignes avez-vous supprimées ? Quel risque d'oubli disparaît avec le computed?
  • Lancer ng generate @angular/core:signal-input-migration sur une copie de PrixAncien et 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/).
  1. 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
  1. À retenir

    • input() déclare une entrée : un signal en lecture seule, rempli par le parent, lu avec nom().
    • input(valeur) fixe une valeur par défaut ; input<T>() peut valoir undefined; 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.
    • alias renomme l'entrée côté parent ; transform convertit la valeur reçue (numberAttribute, booleanAttribute, fonction pure).
    • computed pour une valeur déduite, effect (avec onCleanup) pour une action, linkedSignal pour 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() + ngOnChanges est l'ancienne écriture : à connaître, à migrer vers input().
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.