5. Les outputs et input models

0

Dans le dernier chapitre consacré à Angular, on a vu comment passer des informations d’un composant parent à un composant enfant en utilisant des inputs. Aujourd’hui, on va voir comment renvoyer des données de notre composant enfant à son parent à travers des outputs (qu’on peut aussi appeler des événements). Donc dans ce post, je vais vous expliquer ce que sont les outputs et comment les utiliser. On va également voir comment utiliser les inputs et les outputs afin d’interagir avec des éléments HTML. On va également parler de la nouvelle fonction output qui est disponible à partir d’Angular 17.3, et qui est stable et recommandé à partir de la version 18 d’Angular. Pour finir, on parlera également de la fonction modèle, qui est à la fois un input et un output, et qui va nous permettre de simplifier grandement notre code.

À la fin de ce chapitre, nous aurons une barre de recherche interactive comme celle-ci :

Search bar - with content

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. Je pars du principe que vous avez lu les chapitres précédents.

Résultat final projet Angular

Étant donné que nous souhaitons créer une barre de recherche, commençons par créer un composant « search-bar », en faisant un…

Bash
ng g c components/search-bar

Ensuite, dans le fichier « search-bar.component.html », on va ajouter le code suivant :

search-bar.component.html
<div id="search-bar">
	<input placeholder="Search..." />
	<button><img src="img/search.png" /></button>
</div>

Maintenant, ajoutons un peu de CSS pour rendre le tout un peu plus beau.

search-bar.component.css
#search-bar {
	display: flex;
	background-color: white;
	border-radius: 40px;
	margin: 50px auto;
	width: 400px;
	box-shadow: 3px 3px 10px -3px grey;
	overflow: hidden;
}

input {
	all: unset;
	flex-grow: 1;
	padding: 10px 20px;
}

button {
	border: none;
	margin: 5px;
	padding: 5px;
	border-radius: 40px;
	background-color: rgb(205, 205, 255);
	width: 30px;
	cursor: pointer;
}

button img {
	max-width: 18px;
	max-height: 18px;
}

button:hover {
	background-color: rgb(179, 179, 255);
}

Ensuite, nous allons modifier le contenu de notre fichier « app.component.html ». On va commencer par enlever toutes les cartes à jouer, pour le moment, et on va y ajouter notre barre de recherche.

app.component.html
<div id="search-bar-container">
	<app-search-bar />
</div>

Maintenant, pour vous montrer comment fonctionnent les outputs, j’aimerais que dans notre fichier « app.component.html », on affiche dans un premier temps le nombre de fois que le bouton de recherche a été appuyé. Donc, on y ajoutera un texte « Clicked Count:  » et on va aussi y afficher une propriété « count » qui va stocker le nombre de fois que le bouton a été appuyé.

app.component.html
<div id="search-bar-container">
	<app-search-bar />
</div>
Clicked Count: {{ count }}

Modifiez maintenant notre fichier « app.component.ts » afin d’y créer la propriété « count » et initialisons-la à 0. N’oublions pas non plus de rajouter notre « SearchComponent » aux imports dans « 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';
import { SearchBarComponent } from './components/search-bar/search-bar.component';

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

	count: number = 0;

}

Voici à quoi ressemble le résultat pour l’instant :

first implementation of the search bar and click count

Avant de voir comment créer notre premier output, voyons comment on peut être notifié d’un clic effectué sur notre bouton de recherche. Eh bien, pour cela, Angular nous expose des événements par défaut que nous pouvons utiliser, et ici, l’événement qui nous intéresse est l’événement « click » de notre bouton. Pour pouvoir être notifié de ces événements, on va devoir spécifier, en tant qu’attribut du composant en question, le nom de l’événement auquel on souhaite réagir, entouré de parenthèses. Donc ici, on aura entre parenthèses l’événement click et puis on va devoir spécifier ce qu’on va faire avec l’output de l’événement en question. Pour cela, on va dire que l’événement est égal suivi d’une expression TypeScript, qui peut être une simple assignation d’une variable ou un appel à une fonction, par exemple.

Dans un premier temps, je propose de créer une fonction qui va faire un « console.log » de “Click” afin qu’on puisse confirmer que l’événement « click » appelle bien notre fonction. Pour cela, on modifie le fichier « search-bar.component.ts » comme ceci :

search-bar.component.ts
import { Component } from '@angular/core';

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

	searchClick() {
		console.log('clicked');
	}

}

Là, nous devons encore modifier le fichier « search-bar.component.html » afin d’y associer cette fonction à l’événement « click » :

search-bar.component.html
<div id="search-bar">
	<input placeholder="Search..." />
	<button (click)="searchClick()"><img src="img/search.png" /></button>
</div>

Regardons maintenant ce que nous devons faire afin de créer notre propre événement que le composant « AppComponent » pourra utiliser afin d’être notifié à chaque clic de notre bouton de recherche. Pour cela, on crée une propriété de type « EventEmitter ». Un « EventEmitter »permet, comme son nom l’indique, d’émettre un événement. En plus de cela, on va devoir ajouter le décorateur « @Output() » à notre propriété qu’on va appeler « searchButtonClicked ».

search-bar.component.ts
import { Component, EventEmitter, Output } from '@angular/core';

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

	@Output() searchButtonClicked = new EventEmitter();

	searchClick() {
		console.log('clicked');
	}

}

Maintenant que nous avons notre output, il va falloir émettre un événement à chaque clic. Pour faire ça, on va supprimer notre « console.log » de notre méthode « searchClick ». A la place, on va faire un appel à la méthode « emit » de notre propriété « searchButtonClicked » :

search-bar.component.ts
...
	searchClick() {
		this.searchButtonClicked.emit() ;
	}
...

On peut retourner dans notre fichier « app.component.html » et on va pouvoir utiliser l’output qu’on vient de définir. Pour cela, on va faire la même chose que lorsque nous avons utilisé l’événement « click » du bouton : on va taper « searchClicked » entre parenthèses, et lors de cet événement, on va faire appel à une fonction « increaseCount » qui va incrémenter notre variable « count » :

app.component.html
<div id="search-bar-container">
	<app-search-bar (searchButtonClicked)="increaseCount()" />
</div>
Clicked Count: {{ count }}

Dans notre fichier « app.component.ts », on va créer la méthode « increaseCount » :

app.component.ts
...
	increaseCount() {
		this.count++;
	}
...

Maintenant, si vous exécutez le programme et que vous cliquez sur le bouton « search », vous verrez que le compteur est incrémenté à chaque fois que vous cliquez sur le bouton.

Pour aller plus loin, nous allons examiner comment renvoyer la valeur entrée dans la barre de recherche à notre composant « AppComponent ». Tout d’abord, on va devoir faire un import de « FormsModule » dans le fichier « search-bar.component.ts ». Grâce à cet import, notre input possède maintenant un attribut appelé « ngModel », qui est un input permettant de lier une variable en paramètre avec le contenu à afficher dans l’input.

Créons encore un input dans notre fichier « search-bar.component.ts », que nous appellerons « search » et auquel on va assigner une valeur quelconque par défaut:

search-bar.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
	selector: 'app-search-bar',
	standalone: true,
	imports: [FormsModule],
	templateUrl: './search-bar.component.html',
	styleUrl: './search-bar.component.css'
})
export class SearchBarComponent {

    @Input() search = 'Initial';
	@Output() searchButtonClicked = new EventEmitter();

	searchClick() {
		this.searchButtonClicked.emit() ;
	}

}

Ensuite, on peut ouvrir le fichier « search-bar.component.html » et passer cette propriété au paramètre « ngModel » de l’input afin d’afficher une valeur spécifique à l’intérieur de celui-ci :

search-bar.component.html
<div id="search-bar">
	<input placeholder="Search..." [ngModel]="search" />
	<button (click)="searchClick()"><img src="img/search.png" /></button>
</div>

Si vous lancez votre projet et que vous l’ouvrez dans le navigateur, vous verrez que la valeur dans notre input est maintenant bien initialisée avec la valeur “Initial”.

Search bar with the value "initial"

En plus de cela, l’import du « FormsModule » met aussi un événement à disposition dans notre input, et c’est l’événement « ngModelChange », qui va nous permettre de récupérer la valeur entrée à l’écran, au fur et à mesure qu’elle change. Je propose donc qu’on utilise la valeur retournée par cet événement pour faire appel à une fonction que l’on va appeler « updateSearch » et que l’on va implémenter dans une seconde.

search-bar.component.html
<div id="search-bar">
	<input placeholder="Search..." [ngModel]="search" (ngModelChange)="updateSearch($event)"/>
	<button (click)="searchClick()"><img src="img/search.png"/></button>
</div>

La valeur émise par un événement est stockée dans une variable appelée « $event », et ici nous passons donc cette valeur à notre fonction. Implémentons maintenant la fonction « updateSearch » dans le fichier « search-bar.component.ts », qui devra à son tour émettre la valeur reçue à son parent.

search-bar.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
	selector: 'app-search-bar',
	standalone: true,
	imports: [FormsModule],
	templateUrl: './search-bar.component.html',
	styleUrl: './search-bar.component.css'
})
export class SearchBarComponent {

    @Input() search = 'Initial';
    @Output() searchChange = new EventEmitter<string>();

	@Output() searchButtonClicked = new EventEmitter();

	searchClick() {
		this.searchButtonClicked.emit() ;
	}

	updateSearch(value: string) {
		this.searchChange.emit(value);
	}

}

Pour cela, on crée un output que l’on appelle « searchChange », on ajoute <string> afin d’indiquer que l’événement émis sera une chaîne de caractères, et dans notre fonction « updateSearch » , on fait un « this.searchChange.emit(value) », afin d’émettre la valeur reçue par notre input. Il ne nous reste plus qu’à utiliser cet événement dans notre composant « AppComponent » afin d’utiliser la valeur que l’utilisateur entrera dans notre input. Donc, on ouvre notre fichier « app.component.ts » et on va commencer par créer une variable « search » qu’on va initialiser avec une chaîne de caractères vide.

app.component.ts
...
export class AppComponent {
...
	count: number = 0;
	search: string = '';
...
}

Puis, dans notre fichier « app.component.html », on va assigner notre variable « search » à l’input « search ». En plus de cela, on va assigner la valeur émise par l’événement « searchChange » à notre variable « search ». Pour finir, on va afficher cette variable « search » à l’écran:

app.component.html
<div id="search-bar-container">
	<app-search-bar
		(searchButtonClicked)="increaseCount()"
		[search]="search"
		(searchChange)="search = $event"
	/>
</div>
Search: {{ search }}<br />
Clicked Count: {{ count }}

Maintenant, si on ouvre notre navigateur et qu’on tape quelque chose dans la barre de recherche, on voit que la propriété « search » de notre composant « AppComponent » est automatiquement mise à jour:

Synched test between input and search property of parent component

Une chose importante à savoir est que lorsque vous avez un output qui a le même nom qu’un input, suivi de « Change », et que vous souhaitez que l’output assigne simplement la valeur de celui-ci à la variable que vous avez utilisée en input (comme dans notre exemple ci-dessus), vous pouvez utiliser une notation raccourcie afin d’arriver au même résultat. La notation en question consiste à entourer le nom de votre input de parenthèses, elles-mêmes entourées de crochets, et d’assigner votre propriété à cette expression.

app.component.html
<div id="search-bar-container">
	<app-search-bar
		(searchButtonClicked)="increaseCount()"
		[(search)]="search"
	/>
</div>
Search: {{ search }}<br />
Clicked Count: {{ count }}

On appelle cette manière de lier une variable, à la fois en tant qu’input et output, le « two-way binding », par contraste à un simple input, ou un simple output qui sont des « one-way binding », donc des liaisons qui vont dans un seul sens. Si on sauvegarde et qu’on retourne voir le résultat, on voit que rien n’a changé et que notre variable « search » est bien mise à jour à chaque fois qu’on tape quelque chose dans notre barre de recherche.

Synched test between input and search property of parent component

Il y a une dernière chose que j’aimerais mentionner avant de passer à la nouvelle fonctionnalité « output » : notre « @Output » peut prendre un alias en paramètre. Un alias peut être utile si vous souhaitez donner un nom à votre sortie qui est différent du nom de la variable de votre « EventEmitter ». Imaginons par exemple que nous voulions que notre événement « searchButtonClicked » soit appelé « submit », sans pour autant changer le nom de notre variable. Il suffit pour cela de reprendre notre fichier « search-bar.component.ts » et d’ajouter la chaîne de caractères « submit » en paramètre à notre « @Output » :

search-bar.component.ts
...
	@Output('submit') searchButtonClicked = new EventEmitter<void>();
...

Maintenant nous devons également mettre à jour notre fichier « app.component.html » afin que celui-ci utilise notre alias:

app.component.html
<div id="search-bar-container">
	<app-search-bar
		(submit)="increaseCount()"
		[search]="search"
		(searchChange)="search = $event"
	/>
</div>
Search: {{ search }}<br />
Clicked Count: {{ count }}

Là aussi, si vous ouvrez votre navigateur et cliquez sur le bouton de recherche, vous verrez que votre compteur de clics est bien mis à jour.

Synched test between input and search property of parent component

Nous pouvons maintenant passer à la nouvelle fonction output. Depuis Angular 17.3, il existe donc une nouvelle manière de définir des outputs sans passer par le décorateur « @Output » , et qui ressemble à la manière dont on définit des « signal inputs ».

Avant de continuer, vérifiez que vous êtes sous Angular 17.3 ou plus en tapant la commande suivante dans votre terminal, à l’intérieur du dossier source de votre projet :

Bash
ng version

Si ce n’est pas le cas, mais que votre version actuelle est une version 17.x, exécutez un « npm update » dans la console. Si votre version est antérieure à Angular 17, il va falloir faire « ng upgrade » avec une procédure qui peut différer, la procédure peut différer selon votre version exacte d’Angular et vous trouverez tous les détails ici.

Une fois que vous avez installé une version d’Angular supérieure à la version 17.3, vous pouvez utiliser la nouvelle fonction output. Pour cela, il suffit de retourner dans le fichier « search-barcomponent.ts » et d’effacer les « @Output » , et on remplace les « newEventEmitter » par la fonction output, qui doit être importée d’ « @angular/core ». Donc, si on prend notre exemple, on se retrouve avec le code suivant :

search-bar.component.ts
import { Component, EventEmitter, Input, Output, output } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
    selector: 'app-search-bar',
    standalone: true,
    imports: [FormsModule],
    templateUrl: './search-bar.component.html',
    styleUrl: './search-bar.component.css'
})
export class SearchBarComponent {

    @Input() search = 'blala';
    @Output() searchChange = new EventEmitter<string>();

    searchButtonClicked = output<void>();

    searchClick() {
        this.searchButtonClicked.emit();
    }

    updateSearch(value: string) {
        this.searchChange.emit(value);
    }

}

À part cela, rien ne change. On a toujours la fonction « emit » pour émettre une valeur vers le composant parent. En revanche, si vous essayez votre code maintenant, vous verrez que le compteur de clics ne fonctionne plus, car nous avions défini un alias précédemment qui n’existe plus. Ainsi, notre événement ne s’appelle plus « submit », mais s’appelle de nouveau « searchButtonClicked », comme le nom de notre variable. Bien entendu, ici aussi dire à notre output que nous souhaitons utiliser un alias. Pour cela, on passe un dictionnaire à la fonction output avec la clé « alias » et en valeur notre alias. Ici, nous allons l’appeler « submit ».

search-bar.component.ts
...
	searchButtonClicked = output<void>({alias: 'submit'});
...

Maintenant, si on sauvegarde et qu’on retourne voir notre navigateur, on voit que le compteur fonctionne de nouveau.

Synched test between input and search property of parent component

Là, on pourrait changer nos annotations « @Input » et « @Output » de nos propriétés « search » et « searchChange » en utilisant les fonctions input et output comme ceci:

search-bar.component.ts
import { Component, input, output } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
    selector: 'app-search-bar',
    standalone: true,
    imports: [FormsModule],
    templateUrl: './search-bar.component.html',
    styleUrl: './search-bar.component.css'
})
export class SearchBarComponent {

    search = input('blala');
	searchChange = output<string>();

    searchButtonClicked = output<void>({alias: 'submit'});

    searchClick() {
        this.searchButtonClicked.emit();
    }

    updateSearch(value: string) {
        this.searchChange.emit(value);
    }

}

Sans oublier que, comme maintenant l’attribut « search » est un signal input, on va devoir modifier notre « search-bar.component.html » en ajoutant des parenthèses à « search » afin de passer la valeur du signal à notre « ngModel » et non pas le signal en lui-même:

search-bar.component.html
<div id="search-bar">
	<input placeholder="Search..." [ngModel]="search()" (ngModelChange)="updateSearch($event)"/>
	<button (click)="searchClick()"><img src="img/search.png"/></button>
</div>

Donc comme je le disais plus haut, on aurait pu faire ça, mais il existe depuis Angular 17.2 une nouvelle fonction qui permet de simplifier ce code, et c’est la fonction « model » qui est, tout comme la fonction input, un signal. Pour utiliser cette fonction « model », on va retourner dans notre fichier « search-bar.component.ts » et l’importer de « @angular/core », on va remplacer la fonction input par la fonction « model » , et on va supprimer notre output « searchChange »:

search-bar.component.ts
import { Component, model, output } from '@angular/core';
...
	search = model('Initial');
...

Ce « model input » va non seulement créer un input tout à fait standard, mais également un output avec le nom de votre input suivi de « Change », donc ici un output appelé « searchChange ». Le model input va ensuite se charger d’émettre un événement à chaque fois que sa valeur est changée.

Afin d’utiliser ce « model input », nous devons encore faire quelques changements, le premier est que, dans notre fonction « updateSearch », on ne doit plus utiliser la fonction « emit », car notre « model input » n’a pas de fonction « emit », à la place on va devoir faire un set qui va assigner la valeur de notre barre de recharge à notre « model input » :

TypeScript
...
    updateSearch(value: string) {
        this.search.set(value);
    }
...

Comme je le disais avant, à chaque fois que ce « set » sera exécuté, le « model input » se chargera d’émettre l’événement « searchChange » avec la nouvelle valeur. On peut sauvegarder et vérifier que la valeur de notre « search » est bien mise à jour dans notre composant principal:

Synched test between input and search property of parent component

Maintenant tout fonctionne, mais on peut encore simplifier notre code grâce à ce « model input » . Pour cela, on va retourner dans notre fichier « search-bar.component.html », et là, on va regrouper les attributs « ngModel » et « ngModelChange » en un seul « ngModel » entouré de parenthèses et de crochets. On va lui passer notre « model input » sans parenthèses en tant que paramètre:

search-bar.component.html
<div id="search-bar">
	<input placeholder="Search..." [(ngModel)]="search" />
	<button (click)="searchClick()"><img src="img/search.png" /></button>
</div>

Maintenant grâce à ce two-way binding, « ngModel »se chargera de lire la valeur de notre « model input », et en cas de modifications du texte entré dans la barre de recherche, »ngModel »se chargera également d’assigner la nouvelle valeur à notre « model input ». On peut donc effacer la fonction « updateSearch » dans notre fichier « search-bar.component.ts » qui au final ressemblera à ça:

search-bar.component.ts
import { Component, model, output } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
    selector: 'app-search-bar',
    standalone: true,
    imports: [FormsModule],
    templateUrl: './search-bar.component.html',
    styleUrl: './search-bar.component.css'
})
export class SearchBarComponent {

    search = model('Initial');
    searchButtonClicked = output<void>({alias: 'submit'});

    searchClick() {
        this.searchButtonClicked.emit();
    }

}

Voilà, on a fait le tour de ce long chapitre. Ici aussi, je vous rappelle que je n’ai pas encore expliqué les signaux dans le détail. Donc, si vous ne voyez pas l’intérêt des signaux, ne vous en faites pas, car le prochain chapitre sera entièrement dédié à l’explication des signaux.

Notez qu’à partir de la version 18 d’Angular les signal inputs, le signal outputs et les model inputs sont stables et il est recommandé de les utiliser tout nouveau projet !

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