Angular 2 - Part III - Return of the Angular (or not) ?

Nous continuons la série d'articles autour d'Angular 2, à savoir:


Ici, nous allons plutôt regarder les formulaires, ainsi que des notions un peu plus avancé sur les filtres, les composants.

Ajouter un nouveau super-héros

Tout d'abord, nous allons mettre à jour le service "SuperHeroesService":

export default class SuperHeroesService {
    /**
     * Add the specified @{SuperHero}
     *
     * @method
     * @param {SuperHero} superhero
     */
    add(superhero) {
        SUPERHEROES.push(superhero);
    }

    /**
     * Generate an empty @{SuperHero}
     *
     * @method
     * @returns {SuperHero}
     */
    createDefaultSuperHero() {
        return new SuperHero(Date.now(), null, null, null);
    }
    // ...
}


Maintenant, nous allons créer un composant "SuperHeroCreate" qui utilisera les formulaires. Nous aurons l’occasion de voir à la fois comment récupérer les données d'un formulaire mais aussi gérer sa validation.

Voici à quoi il ressemble:

/**
 * Component to create a new @{SuperHero}
 *
 * @module app/components/superhero-create
 * @export SuperHeroCreateComponent
 * @version 1.0
 * @since 1.0
 */

// Import Angular 2 and application modules
import { Component, View, Inject } from 'angular2/core';
import { FORM_DIRECTIVES } from 'angular2/common';

// Import needed services & filters
import SuperHeroesService from '../services/superheroes';

@Component({ 'selector': 'superhero-create' })
@View({
    'directives': [FORM_DIRECTIVES],
    'template': `
    <fieldset>
        <legend>Create a new superhero</legend>
        <form (submit)="heroForm.form.valid && createNewSuperHero();" #heroForm="ngForm">
            <label>
                <span>FirstName:</span>
                <input type="text" name="firstName" ngControl="firstName" [(ngModel)]="newSuperhero.firstName" required="required" />
            </label>
            <br />
            <label>
                <span>LastName:</span>
                <input type="text" name="lastName" ngControl="lastName" [(ngModel)]="newSuperhero.lastName" required="required" />
            </label>
            <br />
            <label>
                <span>Age:</span>
                <input type="number" name="age" ngControl="age" [(ngModel)]="newSuperhero.age" required="required" min="22" max="99" />
            </label>
            <br />
            <button type="submit" [disabled]="!heroForm.form.valid">Create new super hero</button>
        </form>
    </fieldset>
     `
})
export default class SuperHeroCreate {
    // Injected services
    /** @property {SuperHeroesService} */
    superHeroesService;

    // Variables
    /** @property {SuperHero} */
    newSuperhero;

    /**
     * @constructor
     * @param {SuperHeroesService} superHeroesService
     */
    constructor(@Inject(SuperHeroesService) superHeroesService) {
        this.superHeroesService = superHeroesService;
        this.newSuperhero = this.superHeroesService.createDefaultSuperHero();
    }

    /**
     * Create the new @{SuperHero}
     *
     * @method
     */
    createNewSuperHero() {
        this.superHeroesService.add(this.newSuperhero);
        this.newSuperhero = this.superHeroesService.createDefaultSuperHero();
    }
}




Commençons par le commencement: l'import des directives. Jusqu'à présent, nous importions les directives une par une. Il est toutefois possible d'importer un lot de directives (via un tableau tout simplement). Et Angular 2 nous en propose de base. Par exemple, nous avons FORM_DIRECTIVES qui permet de récupérer toutes les directives associés à la gestion des formulaires à savoir NgModel, NgForm, NgControl, ...

Ensuite, nous voyons que nous utilisons le classique NgModel pour afficher et récupérer les valeurs du modèle. Ce qui est nouveau est le NgControl. Ce dernier permet de surveiller l'état du champ de saisie. Autrement dit, nous allons pouvoir savoir si ce dernier est valide, invalide, dirty, ... comme dans Angular 1.x.

Cerise sur le gâteau: du moment que cette directive est présente, si le champs est invalide, nous aurons alors la classe CSS '.ng-invalid' qui sera injecté sur le composant. Ce qui nous permettra de la personnaliser.

Pour soumettre le formulaire, nous écoutons l'événement et nous sauvons le super-héros ... uniquement si le formulaire est valide. Pour la savoir, nous plaçons un "ref" sur le formulaire qui point sur le composant "ngForm" et nous permet de connaître l'état du formulaire via l'expression "heroForm.form.valid".

Nous pouvons utiliser également cette expression afin de griser le bouton de soumission. Ce qui nous donne à l'affichage:




Maintenant, regardons le composant "Application". Nous modifions le template en utilisant le nouveau composant.

import SuperHeroCreate from './components/superhero-create';

@Component({ 'selector': 'superheroes-app'}) // Name of the tag
@View({
    'directives': [NgFor, SuperHeroFilter, SuperHeroEntry, SuperHeroCreate],
    '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>
    <br />
    <superhero-create></superhero-create>
    `
})


Et comme vous pouvez le constater, quand nous cliquons sur le bouton, le nouveau super-héros se rajoute bien à la liste !


Notion avancé d'un filtre

Outre le nom que nous pouvons préciser dans le decorator @Pipe, nous pouvons déclarer également une notion de "pureté" via la propriété "pure".

Par défaut à true, cela veut dire que l'objet résultant du pipe est "stateless", à savoir qu'il ne sera rafraîchit que si les paramètres du filtre changent.

Nous pouvons néanmoins changer cette valeur afin que l'objet soit "statefull" et que la vue se rafraîchisse si l'objet d'origine ou l'objet généré par le filtre est modifié dans le temps (par un traitement asynchrone dans le pipe, ajout d'un élément dans le tableau, etc ...)

Modifions le filtre "superHeroesContains"

En effet, si nous nous referons au chapitre précédent, nous avons un soucis: si nous appliquons un filtre, et que nous ajoutons un super-héros qui pourrait être affiché malgré le filtre, ce dernier ne s'affichera pas.

Du coup, nous ajoutons la propriété "pure" sur notre filtre:

@Pipe({ 'name': 'superHeroesContains', 'pure': false })
export default class SuperHeroesContains {
   // ...
}

Malheureusement si nous rechargeons la page et nous commençons à filtrer la collection, voici les erreurs que nous allons trouver dans la console:




Pourquoi ? Car en mode statefull, l'objet retourné par le filtre doit avoir la même référence. De ce fait, nous devons adapté notre code de la façon suivante:

@Pipe({ 'name': 'superHeroesContains', 'pure': false })
export default class SuperHeroesContains {
    heroes = [];

    /**
     * Remove the superhero
     *
     * @method
     * @param {SuperHero[]} [superheroes]
     * @param {Object[]} [parameters]
     */
    transform(superheroes, [tokenToFind]) {
        var foundHeroes = superheroes && tokenToFind ?
            superheroes.filter(superhero => superhero.firstName.indexOf(tokenToFind) >= 0 || superhero.lastName.indexOf(tokenToFind) >= 0)
            : superheroes;

        this.heroes
            .splice
            .apply(
                this.heroes,
                [0, this.heroes.length]
                    .concat(foundHeroes)
            );

        return this.heroes;
    }
}


Et là, tout fonctionnera bien !

Notion avancé d'un composant

Tout comme dans Angular 1.x, un composant (ou directive) possède un cycle de vie. Dans le cas présent, cela se reflète par l'exécution de certaines méthodes sur le composant si ces dernières sont présentes:

  • OnInit: quand le composant est complétement initialisé
  • OnDestroy: quand le composant est supprimé
    • Attention, il faut que le traitement fait à l'intérieure de cette méthode soit synchrone
  • AfterContentInit: une fois que les composants du template sont initialisé
  • AfterViewInit: une fois que le template est initialisé

Certaines méthodes sont également présentes, mais elles sont utiles uniquement si notre composant a pour vocation d'être un élément de formulaire:
  • DoCheck: une fois qu'une demande de vérification du modèle a été fait
  • OnChanges: quand le modèle change
  • AfterContentChecked: quand le modèle des composants du template fini d'être vérifié
  • AfterViewChecked: quand le modèle du template a fini d'être vérifié

Même si ces méthodes ne sont pas présentes, nous pouvons déclarer sur le tag du composant lui-même les événements associées que nous voulons écouter, en l'occurence:

  • ngOnChanges
  • ngOnInit
  • ngDoCheck
  • ngAfterContentInit
  • ngAfterContentChecked
  • ngAfterViewInit
  • ngAfterViewChecked
  • ngOnDestroy
Un live exemple se trouve sur ce lien.


Conclusion

Nous constatons encore une fois de l'évolution d'Angular afin de proposer des solutions pour optimiser le traitement tout en proposant des évolutions notables.

Commentaires

Posts les plus consultés de ce blog

ISO: liens & outils utiles

NodeJs et SSL: une petite analyse

Créer sa commande slack en quelques minutes