4. Les signal inputs

0

Pour ce nouveau chapitre consacré à Angular, on va voir comment passer des paramètres en « input » à nos composants, et comment utiliser ces « inputs » afin d’adapter l’affichage du composant en question.

Dans ce chapitre, on va :

  • voir ce que sont les inputs
  • on verra comment les utiliser, avec des exemples concrets
  • pour finir, on verra des utilisations plus avancées des inputs

Pour rappel, tout au long de ce cours, on va créer une application de gestion de collections de timbres, pièces, figurines, cartes à jouer ou autre. Dans le dernier chapitre, on a vu comment créer un projet Angular. Donc on va partir du principe que vous avez un projet prêt à être utilisé.

application de gestion de collections

Avant de parler d’inputs, je propose qu’on ouvre notre projet et qu’on regarde comment stocker du texte à afficher dans notre composant dans une propriété de celui-ci. Pour cela, on va reprendre notre composant « collection-item-card ». Ouvons le fichier « collection-item-card.ts », et créons une propriété pour chaque donnée affichée à l’écran qu’on souhaite pouvoir modifier facilement. On ne va toucher à l’image pour l’instant, mais nous y reviendrons plus tard.

collection-item-card.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-collection-item-card',
  imports: [],
  templateUrl: './collection-item-card.html',
  styleUrl: './collection-item-card.scss'
})
export class CollectionItemCard {

    name = "Excalibur";
    description = "A legendary sword of unmatched sharpness and history.";
    rarity = "Legendary";
    price = 250;

}

Maintenant, nous pouvons modifier l’html de notre composant afin d’utiliser le contenu de ses propriétés au lieu d’un texte hard-codé dans le HTML. Pour cela, il suffit d’utiliser des doubles accolades dans lesquelles on entrera le nom de notre variable.

collection-item-card.html
<article class="collection-item-card">
  <span class="rarity-chip">{{rarity}}</span>

  <figure class="item-image">
    <img src="img/linx.png" alt="Excalibur Sword" />
  </figure>

  <header class="item-header">
    <h2 class="item-name">{{name}}</h2>
  </header>

  <p class="item-description">
    {{description}}
  </p>

  <footer>
  <p class="item-price">{{price}} €</p>
  </footer>
</article>  

Maintenant, si ont fait un ng serve et qu’on ouvre notre navigateur, on voit que notre carte affiche bien les données que nous avons stockées dans nos propriétés.

Maintenant, si on change l’attribut name de notre fichier TypeScript en « Linx »:

On voit que le texte affiché à l’écran s’est bien adapté au contenu de la propriété name. Tout cela est bien beau, mais notre texte est toujours hard-codé dans notre composant et si on modifie notre fichier « app.html » afin d’y utiliser deux fois notre composant, comme ceci:

app.html
<section class="collection-grid">
    <app-collection-item-card></app-collection-item-card>
    <app-collection-item-card></app-collection-item-card>
</section>

et qu’on rajoute un peu de css à notre fichier « app.scss »:

app.scss
.collection-grid {
    display: flex;
    width: 100%;
    gap: 1rem;
    flex-wrap: wrap;
    justify-content: center;
}

on se retrouve avec deux cartes qui ont exactement le même contenu:

Définition

Alors comment faire pour avoir des objets de collections avec des textes différents sans pour autant créer autant de composants que d’objets ? Eh bien, pour cela, on peut utiliser des inputs. Un « input » est une valeur qu’on peut passer en entré à notre composant et que nous pouvons ensuite utiliser dans notre composant.

Si on retourne voir notre fichier « collection-item-card.component.ts » et qu’on souhaite passer les propriétés qu’on vient de créer en input à notre composant, il suffit d’importer la fonction input de @angular/core. Il ne nous reste plus qu’à assigner un input à chaque propriété que l’on veut exposer en entrée, en utilisant la fonction input().

La fonction input, appelée « signal input », prend la valeur par défaut que vous souhaitez assigner à votre propriété en paramètre et retourne un signal, qu’on pourra lire par la suite en l’appelant comme une fonction là où nous voulons y accéder. Si vous n’êtes pas encore familier avec la notion de signals, ne vous en faites pas : un chapitre complet y sera consacré.

Donc si nous avons le signal input suivant:

TypeScript
nom = input("Sergio");

et que nous souhaitons accéder au nom dans notre TypeScript ou dans notre template HTML, nous pouvons le faire en utilisant l’attribut de la manière suivante:

TypeScript
nom()

Utilisation d’input

Maintenant adaptons notre code afin d’utiliser des inputs. Pour cela, commençons par notre fichier TypeScript :

collection-item-card.ts
import { Component, input } from '@angular/core';

@Component({
  selector: 'app-collection-item-card',
  imports: [],
  templateUrl: './collection-item-card.html',
  styleUrl: './collection-item-card.scss'
})
export class CollectionItemCard {

  name = input("Linx");
  description = input("A legendary sword of unmatched sharpness and history.");
  rarity = input("Legendary");
  price = input(250);

}

Adaptons encore notre HTML, afin d’y faire appel aux différents inputs en utilisant des parenthèses :

collection-item-card.html
<article class="collection-item-card">
  <span class="rarity-chip">{{rarity()}}</span>

  <figure class="item-image">
    <img src="img/linx.png" alt="Excalibur Sword" />
  </figure>

  <header class="item-header">
    <h2 class="item-name">{{name()}}</h2>
  </header>

  <p class="item-description">
    {{description()}}
  </p>

  <footer>
  <p class="item-price">{{price()}} €</p>
  </footer>
</article>

Maintenant qu’on a fait ça, on peut retourner dans notre fichier « app.html » afin d’y utiliser nos différents inputs. Pour cela, il suffit d’ajouter des attributs à notre balise HTML avec le nom des propriétés que nous venons de définir. Donc si nous souhaitons changer le nom de l’un de nos objets, on peut faire la chose suivante :

app.html
<section class="collection-grid">
    <app-collection-item-card></app-collection-item-card>
    <app-collection-item-card name="Héro"></app-collection-item-card>
</section>

Et si on retourne voir le résultat dans le navigateur, on voit bien que le deuxième objet a été renommé :

En revanche, si maintenant on essaye de faire la même chose pour l’attribut price et qu’on essaye de le configurer à 20, on voit qu’on obtient une erreur qui nous indique que notre attribute prend un number en paramètre et non une chaîne de caractères.

Pas de panique, c’est tout à fait normal et Angular nous propose un moyen de passer des expressions TypeScript en tant que valeur de nos inputs. Pour cela il suffit d’utiliser le nom de la propriété entourée de crochets :

app.html
<section class="collection-grid">
    <app-collection-item-card></app-collection-item-card>
    <app-collection-item-card name="Héro" [price]="20"></app-collection-item-card>
</section>

A noter que nous aurions pu passer n’importe quelle expression TypeScript en paramètre, que ce soit une propriété de notre composant, un calcul, ou comme ici, un valeur primitive.

Pour illustrer qu’on peut aussi passer des objets typescript en tant qu’input à nos composants je propose de créer un modèle qui va contenir toutes les données liés à nos montres. Pour cela on va créer un nouveau dossier qu’on va appeler « models » à l’intérieur de notre dossier « app », et là, on va créer un fichier qu’on va appeler « collection-item.ts ». Dans ce fichier on va créer la classe CollectionItem qui va contenir toutes les informations liées à un objet de collection.

collection-item.ts
export class CollectionItem {

    name = "Linx";
    description = "A legendary sword of unmatched sharpness and history.";
    image = "img/linx.png"
    rarity = "Legendary";
    price = 250;

}

Maintenant qu’on a notre classe CollectionItem changeons notre fichier « collection-item-card.ts » afin qu’il n’utilise plus qu’un seul input de type CollectionItem:

collection-item-card.ts
import { Component, input } from '@angular/core';
import { CollectionItem } from '../../models/collection-item';

@Component({
  selector: 'app-collection-item-card',
  imports: [],
  templateUrl: './collection-item-card.html',
  styleUrl: './collection-item-card.scss'
})
export class CollectionItemCard {

  item = input(new CollectionItem());

}

Une fois qu’on a fait cela, VS Code va nous afficher des erreurs dans notre fichier « collection-item-card.html », car les attributs que nous avons utilisé dans notre template HTML n’existent plus dans notre component. Au lieu de cela, il faut accéder aux propriétés de l’input item que nous venons de créer :

collection-item-card.html
<article class="collection-item-card">
  <span class="rarity-chip">{{item().rarity}}</span>

  <figure class="item-image">
    <img src="img/linx.png" alt="Excalibur Sword" />
  </figure>

  <header class="item-header">
    <h2 class="item-name">{{item().name}}</h2>
  </header>

  <p class="item-description">
    {{item().description}}
  </p>

  <footer>
    <p class="item-price">{{item().price}} €</p>
  </footer>
</article>

Une fois que nous avons fait cela, il faut aussi modifier notre fichier « app.html », qui, pour l’instant, utilise les attributs name et price sur l’un de nos objets. Pour corriger cela, nous devons d’abord créer un attribut de type CollectionItem dans notre fichier
« app.ts » et lui assigner une nouvelle instance de CollectionItem:

app.ts
import { Component } from '@angular/core';
import { CollectionItemCard } from "./components/collection-item-card/collection-item-card";
import { CollectionItem } from './models/collection-item';

@Component({
  selector: 'app-root',
  imports: [CollectionItemCard],
  templateUrl: './app.html',
  styleUrl: './app.scss'
})
export class App {

  coin!: CollectionItem; 
  
  constructor() {
    this.coin = new CollectionItem();
    this.coin.name = 'Pièce de 1972';
    this.coin.description = 'Pièce de 50 centimes de francs.';
    this.coin.rarity = 'Commune';
    this.coin.image = 'img/coin1.png';
    this.coin.price = 170;
  }

}

Et ensuite, nous pouvons adapter le fichier « app.html » comme ceci:

app.html
<section class="collection-grid">
    <app-collection-item-card></app-collection-item-card>
    <app-collection-item-card [item]="coin"></app-collection-item-card>
</section>

Et voilà, nous avons maintenant nos deux objets distincts :

Regardons encore comment modifier également l’image de notre composant. Pour cela, dans notre fichier « collection-item-card.html », nous pouvons utiliser l’input [src] mis à disposition par la balise img. On peut donc faire la chose suivante:

collection-item.html
<article class="collection-item-card">
  <span class="rarity-chip">{{item().rarity}}</span>

  <figure class="item-image">
    <img [src]="item().image" alt="Excalibur Sword" />
  </figure>

  <header class="item-header">
    <h2 class="item-name">{{item().name}}</h2>
  </header>

  <p class="item-description">
    {{item().description}}
  </p>

  <footer>
    <p class="item-price">{{item().price}} €</p>
  </footer>
</article>

Et maintenant, nous avons bien l’image de notre pièce qui s’affiche:

Utilisation avancée

Maintenant que tout fonctionne revenons un peu plus en détail sur notre input, car il y a encore quelques options très utiles que nous devons mentionner.

Typer les inputs

Premièrement, il est important de noter que la valeur par défaut d’un input est optionnelle, donc si on ne la renseigne pas, Angular n’a aucun moyen de savoir quel type de valeur on attend de notre input. Pour renseigner le type de votre input, il suffit de l’indiquer entre les symboles plus grand ‘<‘ et plus petit ‘>’ comme ceci:

TypeScript
coin = input<CollectionItem>();

Type de retour

La fonction input, retourne un objet du type InputSignal<T> donc dans notre exemple précédent coin a le type suivant :

TypeScript
coin: InputSignal<CollectionItem> = input<CollectionItem>();

Input obligatoire

Vous pouvez rendre un input obligatoire, pour cela vous pouvez utiliser la fonction input.required de manière identique à un input simple. Si dans notre exemple on souhaite rendre l’input item de notre collection-item-card obligatoire, on doit faire la chose suivante:

collection-item-card.ts
import { Component, input } from '@angular/core';
import { CollectionItem } from '../../models/collection-item';

@Component({
  selector: 'app-collection-item-card',
  imports: [],
  templateUrl: './collection-item-card.html',
  styleUrl: './collection-item-card.scss'
})
export class CollectionItemCard {

  item = input.required<CollectionItem>();

}

Important: Remarquez que nous ne pouvons pas passer de valeur par défaut à un input.required, étant donné que l’utilisateur de notre composant sera forcé de le passer en paramètre.

Important 2: Notez également que cette fois-ci nous avons renseigné le type de l’input, car Angular ne peut plus l’inférer automatiquement.

Une fois que nous avons fait ce changement, nous avons un message d’erreur dans notre fichier app.html indiquant que le paramètre item manque à l’une de nos collection-item-cards :

Pour corriger cela, créons un deuxième objet que nous allons appeler linx et gardons toutes les valeurs par défaut de CollectionItem :

app.ts
import { Component } from '@angular/core';
import { CollectionItemCard } from "./components/collection-item-card/collection-item-card";
import { CollectionItem } from './models/collection-item';

@Component({
  selector: 'app-root',
  imports: [CollectionItemCard],
  templateUrl: './app.html',
  styleUrl: './app.scss'
})
export class App {

  coin!: CollectionItem;
  linx!: CollectionItem;
  
  constructor() {
    this.coin = new CollectionItem();
    this.coin.name = 'Pièce de 1972';
    this.coin.description = 'Pièce de 50 centimes de francs.';
    this.coin.rarity = 'Commune';
    this.coin.image = 'img/coin1.png';
    this.coin.price = 170;

    this.linx = new CollectionItem();
  }

}

Et maintenant, assignons cet objet à notre collection-item-card :

app.html
<section class="collection-grid">
    <app-collection-item-card [item]="linx"></app-collection-item-card>
    <app-collection-item-card [item]="coin"></app-collection-item-card>
</section>

Une fois que c’est fait, notre programme fonctionne à nouveau comme avant :

Les alias

Il peut nous arriver de vouloir utiliser un d’attribut différent de celui que nous voulons exposer aux utilisateurs de votre composant. Imaginons par exemple que nous souhaitions que les utilisateurs de notre composant collection-item-card aient un input avec le nom collection-item, mais que voulions continuer à utiliser l’attribut item à l’intérieur de notre composant. Et bien pour cela nous pouvons passer en tant que deuxième paramètre d’un input, ou en tant que seul paramètre d’un input.required, un dictionnaire avec différents attributs dont l’attribut alias. L’attribut alias permet justement de définir un alias au nom de notre variable qui devra être utilisé par les utilisateurs de notre composant :

TypeScript
item = input.required<CollectionItem>({
  alias: 'collection-item'
});

Une fois l’alias défini, l’utilisateur de notre composant doit y faire appel de la manière suivante:

HTML
<app-collection-item-card [collection-item]="coin"></app-collection-item-card>

Je vous propose de tester ceci dans votre code, et puis d’enlever l’alias car on en aura pas besoin pour le reste du cours.

Les transformations

En plus du paramètre alias, il y a un deuxième paramètre que nous pouvons passer de manière identique à input et c’est le paramètre transform. Ce paramètre prend une fonction qui sera appliqué sur notre input avant de le stocker. Nous pouvons donc transformer la valeur reçue avant qu’elle soit stocké dans notre variable. Imaginons par exemple que nous aimerions convertir le prix de notre objet en dollars avant de l’afficher; pour cela, on pourrait faire la chose suivante:

TypeScript
  item = input.required<CollectionItem, CollectionItem>({
    transform: (value: CollectionItem) => {
      value.price = value.price * 1.17;
      return value;
    }
  });

Notez que nous passons deux types à input.required. Le premier type indique le type qui sera retourné par notre input suite à l’utilisation de notre transformation, et le deuxième indique le type de la valeur que l’utilisateur de notre composant va devoir renseigner.

Donc si on voudrait que notre transformation retourne uniquement le prix en dollars de notre objet, on pourrait faire la chose suivante :

TypeScript
  item = input.required<number, CollectionItem>({
    transform: (value: CollectionItem) => {
      return value.price * 1.17;
    }
  });

Après avoir testé le code ci-dessus, n’oubliez pas de remettre votre code tel qu’on l’avait avant le sous-chapitre alias, car le reste de cours par du principe qu’on a un input simple, sans alias ni transformations:

TypeScript
item = input.required<CollectionItem>();

Conclusion

Et voilà, on a fait le tour de tout des signal inputs. Dans notre prochain chapitre, on verra comment renvoyer des valeurs aux utilisateurs de nos composants en utilisant des signal outputs.

Ressources

Vous retrouvez ci-dessous un lien vers le code source utilisé dans ce chapitre. Ce lien est uniquement disponible pour les abonnés Standard et Premium. 

Quiz

Répondez au quiz ci-dessous afin de vérifier que vous avez bien compris le contenu de cette leçon. Les quiz sont uniquement disponibles pour les abonnés Standard et Premium.

3 Comments on “4. Les signal inputs

    1. Bonjour et merci,

      le coin!: CollectionItem; défini un attribut de type CollectionItem, et le ! signale au compilateur TypeScript qu’on est sûr que l’attribute aura une valeur au moment ou on l’utilisera. Ici on l’assigne directement dans le constructeur. Sans le !, on aurait du directement assigner une valeur à coin, ou lui donner un type CollectionItem | undefined;

Laisser un commentaire