4. Les inputs et signal inputs

0

Pour ce nouveau post 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. Du coup, on va expliquer ce que sont les « inputs » et comment les utiliser. On va aussi expliquer ce que sont les « outputs ». Pour finir, nous allons également voir ce que sont que les « signal inputs » ainsi que les « signal outputs », et nous regarderons comment modifier notre code afin de les utiliser.

Pour rappel, cette série de posts s’inscrit dans une longue lignée de posts dont le fil rouge est la création d’une application de visualisation et de gestion de cartes à collectionner de type Pokémon, Magic, Yu-Gi-Oh! ou autre, et je pars du principe que vous avez lu les chapitres précédents.

Résultat final projet Angular

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 « playing-card ». On ouvre le fichier « playing-card.component.ts », et on va créer une propriété pour chaque donnée affichée à l’écran qu’on souhaite pouvoir modifier facilement.

playing-card.component.ts
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';

@Component({
	selector: 'app-playing-card',
	standalone: true,
	imports: [CommonModule],
	templateUrl: './playing-card.component.html',
	styleUrl: './playing-card.component.css'
})
export class PlayingCardComponent {
	name: string = "My Monster";
	hp: number = 40;
	figureCaption: string = "N°001 Monster";
	attackName: string = "Geo Impact";
	attackStrength: number = 60;
	attackDescription: string = "This is a long description of a monster capacity.";
}

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.

playing-card.component.html
<div id="card">
	<div id="inside">
		<header>
			<div class="left">
				<div id="name">{{name}}</div>
			</div>
			<div class="right">
				<div id="hp">{{hp}} HP</div>
				<img class="energy icon" src="img/electric.png">
			</div>
		</header>
		<figure id="art">
			<img src="img/pika.png" />
			<figcaption>{{figureCaption}}</figcaption>
		</figure>
		<div id="capacities">
			<div class="capacity">
				<div class="main">
					<div class="cost">
						<img class="icon energy" src="img/electric.png"/>
						<img class="icon energy" src="img/electric.png"/>
					</div>
					<div class="name">{{attackName}}</div>
					<div class="damage">{{attackStrength}}</div>
				</div>
			</div>
			<div class="description">{{attackDescription}}</div>
		</div>
	</div>
</div>

Maintenant, si vous faites un « ng serve » et que vous ouvrez votre navigateur, vous verrez votre carte avec les données que vous avez stockées dans vos propriétés.

First input variables displayed

Maintenant, si on change la variable « figureCaption » de notre fichier « .ts » en « N°001 My Monster »:

paying-card.component.ts
  figureCaption: string = "N°001 My Monster";

Vous voyez que le texte affiché à l’écran s’est bien adapté au contenu de la propriété « figureCaption ».

Input Variables Variation

Tout cela est bien beau, mais notre texte est toujours hard-codé dans notre composant et si on modifie notre fichier « app.component.ts » pour qu’il utilise le composant deux fois, comme ceci:

app.component.ts
<div id="card-list">
	<app-playing-card />
	<app-playing-card />
</div>

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

app.component.css
...

#card-list {
	display: flex;
	gap: 20px;
	flex-wrap: wrap;
	align-items: center;
	justify-content: center;
}

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

Same component

Alors comment faire pour avoir des cartes avec des textes différents sans pour autant créer autant de composants que de cartes à jouer ? 

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 comme une propriété tout à fait normale de notre composant.

Depuis Angular 17, il existe deux manières de définir des inputs: une manière qui utilise le décorateur  » @Input() », et une autre méthode qui consiste à utiliser les « Signal Inputs » desquels on parlera un peu plus tard dans l’article. On va donc commencer par le décorateur « @Input() ».

Si on retourne voir notre fichier « playing-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 l’input de ‘@angular/core’ et puis de rajouter un « @Input() » avant le nom de nos propriétés:

playing-card.component.t
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';

@Component({
	selector: 'app-playing-card',
	standalone: true,
	imports: [CommonModule],
	templateUrl: './playing-card.component.html',
	styleUrl: './playing-card.component.css'
})
export class PlayingCardComponent {

	@Input() name: string = "My Monster";
	@Input() hp: number = 40;
	@Input() figureCaption: string = "N°001 My Monster"
	@Input() attackName: string = "Geo Impact"
	@Input() attackStrength: number = 60;
	@Input() attackDescription: string = "This is a long description of a monster capacity.";

}

Maintenant qu’on a fait ça on peut retourner dans notre fichier « app.component.html » et y ajouter 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.

app.component.html
<div id="card-list">
	<app-playing-card />
	<app-playing-card name="Pik" />
</div>

Si maintenant vous regardez le résultat vous verrez que le nom de la carte à bien été adapté à Pik.

Component Monster - name pik

En revanche si maintenant vous essayez de faire la même chose pour l’attribut hp que vous essayez de le configurer à 20, vous verrez que vous aurez une erreur qui vous indique que votre 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.component.html
<div id="card-list">
	<app-playing-card />
	<app-playing-card [hp]="12" />
</div>

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 calcule, ou comme ici, un valeur primitive.

Component Monster - hp 12

Pour ilustrer 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 la on va créer un fichier qu’on va appeler monster.model.ts et dans ce fichier on va créer la classe Monstre qui va contenir toutes les informations liées à notre monstre.

monster.model.ts
export class Monster {

	name: string = "Monster";
	hp: number = 10;
	figureCaption: string = "N°001 Monster";

	attackName: string = "Standard Attack";
	attackStrength: number = 10;
	attackDescription: string = "A standard attack";

}

Maintenant qu’on a notre classe monster changeons notre fichier playing-card.component.ts afin qu’il n’utilise plus qu’un seul input de type Monster:

playing-card.component.ts
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { Monster } from '../../models/monster.model';

@Component({
	selector: 'app-playing-card',
	standalone: true,
	imports: [CommonModule],
	templateUrl: './playing-card.component.html',
	styleUrl: './playing-card.component.css'
})
export class PlayingCardComponent {

	@Input() monster: Monster = new Monster();

}

Une fois qu’on a fait cela, VSCode va nous afficher des erreurs dans notre fichier « playing-card.component.html », car les attributs que nous avons utilisé dans notre tempalte HTML n’existent plus dans notre component. A la place il faut accéder aux propriétés de l’objet monster:

playing-card.component.html
<div id="card">
	<div id="inside">
		<header>
			<div class="left">
				<div id="name">{{ monster.name }}</div>
			</div>
			<div class="right">
				<div id="hp">{{ monster.hp }} HP</div>
				<img class="energy icon" src="img/electric.png" />
			</div>
		</header>
		<figure id="art">
 			<img src="img/pika.png" />
 			<figcaption>{{ monster.figureCaption }}</figcaption>
 		</figure>
		<div id="capacities">
			<div class="capacity">
				<div class="main">
					<div class="cost">
						<img class="icon energy" src="img/electric.png" />
						<img class="icon energy" src="img/electric.png" />
					</div>
					<div class="name">{{ monster.attackName }}</div>
					<div class="damage">{{ monster.attackStrength }}</div>
 				</div>
 			</div>
 			<div class="description">{{ monster.attackDescription }}</div>
 		</div>
	</div>
</div>

Une fois que nous avons fait cela, il faut aussi modifier notre fichier app.component.html qui pour l’instant utilise l’attribut hp pour changer les hp d’une de nos deux cartes. Pour corriger cela nous devons d’abord créer une propriété de type monstre dans notre fichier app.component.ts:

app.component.ts
import { Component } from '@angular/core';
import { PlayingCardComponent } from './components/playing-card/playing-card.component';
import { Monster } from './models/monster.model';

@Component({
	selector: 'app-root',
	standalone: true,
	imports: [PlayingCardComponent],
	templateUrl: './app.component.html',
	styleUrl: './app.component.css'
})
export class AppComponent {

	monster1!: Monster;

	constructor() {
		this.monster1 = new Monster();
		this.monster1.name = "Pik";
		this.monster1.hp = 40;
		this.monster1.figureCaption = "N°002 Pik";
	}

}

Et ensuite nous pouvons adapter le fichier app.component.html comme ceci:

app.component.html
<div id="card-list">
	<app-playing-card />
	<app-playing-card [monster]="monster1" />
</div>

Et voici le rendu de nos deux cartes:

Angular component using a monster as input

Maintenant que tout fonctionne revenons sur notre décorateur input, car il y a encore quelques options très utiles que nous pouvons lui passer en paramètre:

playing-card.component.ts
import { CommonModule } from '@angular/common';
import { Component, Input } from '@angular/core';
import { Monster } from '../../models/monster.model';

@Component({
	selector: 'app-playing-card',
	standalone: true,
	imports: [CommonModule],
	templateUrl: './playing-card.component.html',
	styleUrl: './playing-card.component.css'
})
export class PlayingCardComponent {

	@Input({
		required: true,
		alias: 'my-monster',
		transform: (value: Monster) => {
			value.hp = value.hp / 10;
			return value;
		}
	}) monster: Monster = new Monster();

}

Analysons les paramètres que nous avons passé à notre décorateur. Tout d’abord nous avons le paramètre « required ». Ce paramètre nous indique que l’input monster est obligatoire et donc si on ne le renseigne pas nous aurons une erreur.

Le deuxième paramètre est le paramètre « alias ». Ce paramètre nous permet de choisir un nom pour l’attribute HTML qui sera utilisé pour passer une valeur à notre input, donc ici notre propriété « monster », va contenir la valeur qu’on passer à l’attribut « my-monster » à la balise « app-playing-card ».

Pour finir le paramètre transform prend une fonction en paramètre, cette fonction va prendre la valeur passé en input par l’utiliser et la transformer avant de la stoquer dans la propriété en question. Ici on prend le montre passé en paramètre et on lui divise ses points de vie par 10 avant de le stoquer, mais on aurait pu faire n’importe quoi d’autre et on aurait même pu convertir ce type monstre en un autre type si nécessaire, par example en returnant uniquement les points de vie du monstre et en les stoquant dans une propiété de type number.

Maintenant afin de que notre code fonctionne de nouveau on va devoir apporter quelques changements à notre fichier app.compoment.html:

app.component.html
<div id="card-list">
	<app-playing-card [my-monster]="monster1" />
	<app-playing-card [my-monster]="monster1" />
</div>

Comme l’attribute my-monster est maintenant obligatoire, je l’ai assigné deux fois à « monster1 », mais bien entendu vous pouvez créer une deuxième variable « monster2 » avec des valeurs spécifiques.

Component with input paramètres

Il est temps de passer au signal inputs. Depuis Angular 17 il existe une nouvelle manière de définir des inputs, appelée « Singal input », et qui est une brique qui vient s’emboiter dans un une évolution majeur du framework qui tourne autour des signaux. On aura un chapitre dédié aux signaux, mais pour faire court un signal est un genre de wrappeur qui vient se placer autour d’une valeur que soit une valeur primitive (nombre, chaine de caractère, …) ou tout autre type complexe, et qui va pouvoir notifier d’autres parties de votre code qui dependent de cette valeur, lorsque la valeur en question change.

Depuis la version 18 d’Angular, les signaux sont stables et il est recommandé de les utiliser pour tous nouveau projet.

Maintenant que j’ai fait tous mes disclaimers, passons aux signal inputs, et pour les utiliser rien de plus simple, on peut commencer par enlever toutes nos annotation @Input de notre composant « playing-card.component.ts ». Ensuite au lieu d’assigner une valeur directement à notre propriété monster, on va lui assigner un signal input en tapant « input », avec un petit « i », qu’on importe d’ »angular/core » et entre parenthèses on va lui passer notre valeur par défaut, donc notre « new monster() »:

playing-card.component.ts
import { CommonModule } from '@angular/common';
import { Component, InputSignal, input } from '@angular/core';
import { Monster } from '../../models/monster.model';

@Component({
	selector: 'app-playing-card',
	standalone: true,
	imports: [CommonModule],
	templateUrl: './playing-card.component.html',
	styleUrl: './playing-card.component.css'
})
export class PlayingCardComponent {

	monster: InputSignal<Monster> = input(new Monster());

}

Si vous regardez votre fichier « playing-card.component.html » vous verrez que vous avez des erreur, est c’est normal car maintenant notre propriété n’est plus un objet de type Monster mais un signal et pour accéder à la valeur du signal il faut y faire appel en rajoutant des parenthèse après le nom du signal:

playing-card.component.html
<div id="card">
	<div id="inside">
		<header>
			<div class="left">
				<div id="name">{{ monster().name }}</div>
			</div>
			<div class="right">
				<div id="hp">{{ monster().hp }} HP</div>
				<img class="energy icon" src="img/electric.png" />
			</div>
		</header>
		<figure id="art">
 			<img src="img/pika.png" />
 			<figcaption>{{ monster().figureCaption }}</figcaption>
 		</figure>
		<div id="capacities">
			<div class="capacity">
				<div class="main">
					<div class="cost">
						<img class="icon energy" src="img/electric.png" />
						<img class="icon energy" src="img/electric.png" />
					</div>
					<div class="name">{{ monster().attackName }}</div>
					<div class="damage">{{ monster().attackStrength }}</div>
 				</div>
 			</div>
 			<div class="description">{{ monster().attackDescription }}</div>
 		</div>
	</div>
</div>

Et voila on vient de créer et d’utiliser notre premier signal input, bon il faut encore changer notre fichier « app.component.html », car nous n’avons plus notre alias, my-monster et notre input n’est plus obligatoire:

app.component.html
<div id="card-list">
	<app-playing-card />
	<app-playing-card [monster]="monster1" />
</div>

Et maintenant ici notre résultat final:

composant avec signal inputs

Pour information vous pouvez aussi indiquer qu’un signal input est obligatoire en utiliser un input.required au lieu d’un simple input:

playing-card.component.ts
	monster: InputSignal<Monster> = input.required();

Remarquez que j’ai enlevé la valeur par défaut « new Monster() » car comme l’input est obligatoire, il n’accepte pas de valeur par défaut.

Vous pouvez aussi définir des « alias » et des « transform » pour vos signal inputs en passons un dictionnaire avec ces valeurs en input à vos « input » ou « input.required »:

playing-card.component.ts
	monster: InputSignal<Monster> = input(new Monster(), {
		alias: "my-monster",
		transform: (value: Monster) => {
			value.hp = value.hp / 2;
			return value;
		}
	});

ou:

playing-card.component.ts
	monster: InputSignal<Monster> = input.required({
		alias: "my-monster",
		transform: (value: Monster) => {
			value.hp = value.hp / 2;
			return value;
		}
	});

Et voila, on a fait le tour de tout ce que je voulais vous montrer concernant les inputs et les signal inputs. Je vous conseil d’utiliser les signal inputs pour tous vos nouveaux projets, dès qu’ils ne seront plus en « developper preview ». En attendant, vous savez qu’ils existent et vous comprendrez tout leur intérêt dans au fur et à mesure des prochains chapitre, ou on parlera spécifiquement de signaux.

Resources

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.

Laisser un commentaire