Angular 2 - Part II - Angular strikes back (or not) ?
Voici la suite de l'article "Angular 2 - Part I - A new hope (or not) ?".
Dans cet article, nous allons essayer de faire évoluer notre application afin
- D'étendre notre composant et voir le data-binding se mettre en oeuvre
- De mettre en place des filtres
- De mettre en place un petit formulaire
- De voir la mécanique de référence entre les composants et le système événementielle
Découverte du data-binding "simple" dans nos composants
L'objectif va être ici d'ajouter un bouton sur le composant "SuperHeroEntry" afin qu'il se supprime de lui-même de la liste de super-héros. Pour ce faire, nous allons:
- Modifier le service SuperHeroesSerrvice pour ajouter la fonction de suppression
- Modifier son template afin d'intercepter un événement
- Déclarer la fonction liée au bouton dans le composant
Dans un premier temps, le service:
export default class SuperHeroesService { /** * Get all superheroes ! * * @method * @returns {SuperHero[]} */ fetch() { return SUPERHEROES; } /** * Remove the specified @{SuperHero} * * @method * @param {SuperHero} superhero * @returns {boolean} true if the @{SuperHero} is remove */ remove(superhero) { let index = SUPERHEROES.indexOf(superhero); if (index >= 0) { SUPERHEROES.splice(index, 1); } return false; } /** * Count the number of @{SuperHero} * * @method * @returns {number} */ size() { return SUPERHEROES.length; } }
Maintenant, mettons à jour le composant:
@Component({ 'selector': 'superhero-entry' }) @View({ 'directives': [], 'template': ` {{ superhero.toString() }} - <button (click)="removeSuperHero();">Remove me</button> ` }) export default class SuperHeroEntry { // Injected services /** @property {SuperHeroesService} */ superHeroesService; // Passed values /** @property {SuperHero} */ @Input('superhero') superhero; /** * @constructor * @param {SuperHeroesService} superHeroesService */ constructor(@Inject(SuperHeroesService) superHeroesService) { this.superHeroesService = superHeroesService; } /** * Remove the superhero * * @method */ removeSuperHero() { this.superHeroesService.remove(this.superhero); } }
Que constatons-nous ? Tout d'abord, le fameux "ng-click" n'existe plus. Pour déclarer la capture d'un événement (qu'Angular2 traitera), il faut déclarer dans son template: (eventName).
L'avantage de cette approche (et nous le verrons lorsque nous aborderons les composants d'un point de vue plus évolué) est que nous pouvons capturer par ce biais là les événements customs. Ainsi, il n'y a plus la notion de $emit ou $broadcast: nous utilisons la mécanique événementielle du DOM.
Tout comme auparavant toutefois, dans l'expression associé à l'événement, nous pouvons avoir accès à l'événement courant via $event.
Ce qui nous donne à l'affichage:
Et là quand nous cliquons sur l'un des boutons, ...., rien ne marche !! Alors que si nous analysons le code, la méthode "removeSuperHero" du composant est bien appelé, la méthode du service aussi, le super-héros supprimé de la liste. Mais rien ne se rafraîchit.
La notion de zone
L'explication est en faite toute simple. Contrairement à Angular 1.x, nous avons une notion de "zone". Celle-ci représente une portion d'HTML, sait les propriétés à observer pour se mettre à jour, connaît la zone parente et les zones enfants. Cette mécanique permet d'optimiser les mises à jours.
De ce fait, nous devons importer la dépendance via JSPM:
> jspm install zone.js
Une fois cela fait, nous devons l'importer. La logique voudrait que nous l'importions via un "import 'zone.js';" ES6. Le problème est que le packaging fait via TypeScript n'est pas tout à fait compatible avec la norme. Du coup, JSPM va nous lever une exception. De ce fait, pour pallier à ça, nous allons pour l'instant l'importer directement dans le fichier "index.html":
<!-- Base imports --> <script type="text/javascript" charset="utf-8" src="jspm_packages/system.js"></script> <script type="text/javascript" charset="utf-8" src="jspm_packages/npm/zone.js@0.5.10/dist/zone.js"></script> <script type="text/javascript" charset="utf-8" src="config.js"></script>
Et là, tout fonctionne !
Les filtres Angular 2.0
La notion de filtre se nomme désormais "Pipe" dans Angular 2.0. Tout comme pour les composants, nous allons devoir créer une classe représentant le pipe et le déclarer par la suite dans les composants où nous voudrons l'appliquer.
Pour commencer, créons le fichier "app/filters/contains" afin de déclarer un pipe qui cherchera dans une collection de super-héros ceux dont le nom ou le prénom contient une chaîne de caractère spécifique.
/** * Filter to search into a superhero which will contain a specific string in the firstame or lastname * * @module app/filters/contains * @export SuperHeroesContains * @version 1.0 * @since 1.0 */ // Import Angular 2 and application modules import { Pipe } from 'angular2/core'; @Pipe({ 'name': 'superHeroesContains' }) export default class SuperHeroesContains { /** * Remove the superhero * * @method * @param {SuperHero[]} [superheroes] * @param {Object[]} [parameters] */ transform(superheroes, [tokenToFind]) { if (superheroes && tokenToFind) { return superheroes.filter(superhero => superhero.firstName.indexOf(tokenToFind) >= 0 || superhero.lastName.indexOf(tokenToFind) >= 0); } return superheroes; } }
Attardons-nous un peu à ce petit bout de code. Tout d'abord, nous avons le decorator @Pipe. Ce dernier va contenir via la propriété "name" le nom du filtre. A noter que le nom du pipe doit être écrit en camelcase.
Ensuite, sur la classe représentant le pipe, nous devons déclarer la méthode transform. Celle-ci aura deux paramètres. Le premier est la valeur sur laquelle nous voulons appliquer le pipe. Dans notre cas, ce sera une collection, mais ça peut être aussi bien une chaîne de caractères, un nombre, etc ... Le second sera un tableau de paramètres qui ont été injecté lors de l'utilisation du pipe. Comme vous pouvez le voir, nous utilisons la déstructuration ES6 afin de récupérer la valeur qui nous intéresse.
Maintenant, utilisons-le dans le composant "Application" où nous itérons sur une collection de super-héros.
import SuperHeroesContains from './filters/contains'; @Component({ 'selector': 'superheroes-app'}) // Name of the tag @View({ 'directives': [NgFor, SuperHeroEntry], 'pipes': [SuperHeroesContains], 'template': ` <h1>Superheroes list ({{ superHeroesService.size() }})</h1> <br /> <ul> <li *ngFor="#superhero of (superHeroesService.fetch() | superHeroesContains:'on')"> <superhero-entry [superhero]="superhero"></superhero-entry> </li> </ul> ` })
Là encore, nous devons l'importer puis le déclarer en tant que dépendance dans la propriété "pipes" de @View. Après, l'usage se fait comme à l'accoutumé dans Angular 1.x, à savoir avec un pipe (d'où l'usage de @Pipe) et de déclarer les paramètres en les séparant par des ":".
Attention toutefois: il est conseillé d'entourer par des parenthèses l'objet où nous voulons appliquer un pipe. Ainsi, si nous souhaitons faire un enchaînement de filtres, nous devrons bien penser à ajouter les parenthèses.
Ce qui donne bien:
Filtre à valeur dynamique
Maintenant, regardons comment passer une valeur dynamique à notre filtre. Commençons par créer un champ de saisie qui contiendra la valeur à rechercher. Pour ce faire nous allons à la fois modifier le template du composant "Application", mais aussi le nouveau composant NgModel, qui permettra comme dans Angular 1.x de récupérer la saisie.
import { NgFor, NgModel } from 'angular2/common'; @Component({ 'selector': 'superheroes-app'}) // Name of the tag @View({ 'directives': [NgFor, NgModel, SuperHeroEntry], 'pipes': [SuperHeroesContains], 'template': ` <h1>Superheroes list ({{ superHeroesService.size() }})</h1> <br /> <label> <span>Search with:</span> <input type="text" name="containsField" [(ngModel)]="containsField" /> </label> <br /> <ul> <li *ngFor="#superhero of (superHeroesService.fetch() | superHeroesContains:containsField)"> <superhero-entry [superhero]="superhero"></superhero-entry> </li> </ul> ` }) class Application { // Variables /** @property {SuperHeroesService} */ superHeroesService; /** @property {string} */ containsField = ''; /** * @constructor * @param {SuperHeroesService} superHeroesService */ constructor(@Inject(SuperHeroesService) superHeroesService) { this.superHeroesService = superHeroesService; } }
Que pouvons-nous constater ? Tout d'abord, nous devons préciser sur le composant "Application" une propriété afin de récupérer la valeur pour filtrer la collection de super-héros. En l'occurence ici "containsField". Que pourrons le passer ensuite en paramètre de notre filtre.
Mais surtout, c'est la façon d'utiliser ngModel, à savoir [(ngModel)]. Tout d'abord, l'usage des [] indique que nous voulons lire la valeur de l'expression. Tandis que () indique que nous voulons affecter la valeur à l'expression. Ce qui nous indique que nous pouvons faire du data-binding bidirectionnel ou unidirectionnel en fonction de notre besoin.
Composant, ref et @Output
Maintenant, faisons un petit refactoring de notre code vue précédemment. Nous allons créer un composant qui contiendra le champs de recherche. Mais du coup qui aura pour vocation de fournir la valeur saisie et ainsi voir qu'un composant peut digérer une valeur via @Input, mais aussi en produire une.
Tout d'abord, créons le composant "SuperHeroFilter" (qui n'est qu'une reprise du code précédent):
/** * Component to filter a collection of @{SuperHero} * * @module app/components/superhero-filter * @export SuperHeroFilterComponent * @version 1.0 * @since 1.0 */ // Import Angular 2 and application modules import { Component, View } from 'angular2/core'; import { NgModel } from 'angular2/common'; @Component({ 'selector': 'superhero-filter' }) @View({ 'directives': [NgModel], 'template': ` <label> <span>Search with:</span> <input type="text" name="containsField" [(ngModel)]="containsField" /> </label> ` }) export default class SuperHeroFilter { /** @property {string} */ containsField = ''; }
Maintenant, voyons comment nous allons l'utiliser dans le composant "Application":
import SuperHeroesContains from './filters/contains'; @Component({ 'selector': 'superheroes-app'}) // Name of the tag @View({ 'directives': [NgFor, SuperHeroFilter, SuperHeroEntry], 'pipes': [SuperHeroesContains], 'template': ` <h1>Superheroes list ({{ superHeroesService.size() }})</h1> <br /> <superhero-filter #filterComponent></superhero-filter> <br /> <ul> <li *ngFor="#superhero of (superHeroesService.fetch() | superHeroesContains:filterComponent.containsField)"> <superhero-entry [superhero]="superhero"></superhero-entry> </li> </ul> ` })
Première chose, nous déclarons sur le tag "superhero-filter" la propriété #filterComponent. Nous utilisons ici les Angular 2 ref. L'objectif est simple: rendre accessible dans le composant et son template un élément du DOM du dit template. Et l'objet qui y sera associé sera de type HTMLElement.
Et comment récupérer la valeur du filtre ? Tout simplement en faisant un filterComponent.containsField. En effet, le composant possède une propriété "containsField" qui sera publique. Et donc nous pouvons y avoir directement accès et l'utiliser ainsi en paramètre de notre filtre.
Cette approche est intéressante, car c'est comme si nous lisions la propriété "value" d'un champs de saisie. Ce qui montre que nous créons réellement de vrai élément HTML.
Je vois déjà certains d'entre vous dire: "Ouuuuuuuuuui mais on rend publique la propriété, et c'est pas bien".
Certes.
Alors utilisons @Output. Ce dernier a pour vocation de générer un événement où nous passerons notre nouvelle valeur en utilisant la mécanique d'EventEmitter (que nous retrouvons pas exemple dans NodeJs).
Et pour l'intercepté ? Tout simplement en déclarant une fonction dans l'expression associée, et en lisant la valeur associée à $event.
Revoyant notre composant:
import { Component, View, Output, EventEmitter } from 'angular2/core'; import { NgModel } from 'angular2/common'; @Component({ 'selector': 'superhero-filter' }) @View({ 'directives': [NgModel], 'template': ` <label> <span>Search with:</span> <input type="text" name="containsField" [(ngModel)]="containsField" (ngModelChange)="filterUpdate.emit($event)" /> </label> ` }) export default class SuperHeroFilter { @Output('filterUpdate') filterUpdate = new EventEmitter(); }
Nous écoutons l'événement associé au composant NgModel (à savoir ngModelChange). Nous trouverons alors dans $event la valeur du dit NgModel.
Nous déclarons un canal d'événement que nous nommerons "filterUpdate" et nous utilisons à la fois @Output et EventEmitter pour mettre en place ce canal événementiel.
Et pour émettre l'événement, il nous suffira alors de faire un "emit" qui contiendra la future valeur de $event.
Et ensuite dans notre composant "Application":
@Component({ 'selector': 'superheroes-app'}) // Name of the tag @View({ 'directives': [NgFor, SuperHeroFilter, SuperHeroEntry], 'pipes': [SuperHeroesContains], 'template': ` <h1>Superheroes list ({{ superHeroesService.size() }})</h1> <br /> <superhero-filter (filterUpdate)="containsFieldWasUpdated($event);"></superhero-filter> <br /> <ul> <li *ngFor="#superhero of (superHeroesService.fetch() | superHeroesContains:containsField)"> <superhero-entry [superhero]="superhero"></superhero-entry> </li> </ul> ` }) class Application { // Variables /** @property {SuperHeroesService} */ superHeroesService; /** @property {string} */ containsField = ''; /** * @constructor * @param {SuperHeroesService} superHeroesService */ constructor(@Inject(SuperHeroesService) superHeroesService) { this.superHeroesService = superHeroesService; } /** * Called when the containsField was updated * * @method * @param {string} newContainsField */ containsFieldWasUpdated(newContainsField) { this.containsField = newContainsField; } }
Nous voyons que tout comme "ngModelUpdate", nous pouvons récupérer l'événement en déclarant un "filterUpdate" et que $event contiendra la valeur saisie.
Conclusion
Nous constatons ainsi que le data-binding, et la façon de récupérer les événements ou de traiter les événements ont peu de choses en commun avec Angular 1.x.
Néanmoins, cela apporte une richesse à son application et aussi mine de rien une facilité accrue tout en ayant une optimisation dans le cycle de vie Angular.
Commentaires
Enregistrer un commentaire