Angular2 : les directives

Article mis à jour avec la version rc1 !

Dans Angular2 il y a 3 types de directives et nous avons déjà eu l’occasion d’en rencontrer de 2 types depuis le début de cette série d’articles.

Pour résumer on a :

  • les composants : on a vu qu’un composant est en fait une directive avec son sélecteur et son template,
  • les directives structurelles : elles agissent sur le DOM en ajoutant ou retirant des éléments, on a vu NgFor et NgIf,
  • les directives attributs : elles changent l’apparence ou le comportement d’un élément, on n’a pas encore eu l’occasion d’en utiliser mais ça viendra.

On va poursuivre avec l’exemple du chronomètre en montrant comment utiliser plusieurs composants pour le constituer. Pour vous simplifier la vie vous pouvez trouver les fichiers ici pour la situation qu’on a laissée à la fin du précédent article.

Les templates

Pour le chronomètre on a utilisé un composant avec ce template (app.component.html) :

<div class="container">
  <div class="text-center">
    <h1>Le chronomètre</h1>
    <hr>
    <h1>
      <span class="label label-primary">{{_timer.minutes}}</span> minutes
      <span class="label label-primary">{{_timer.secondes}}</span> secondes
    </h1>
    <br>
    <p>
      <button class="btn btn-danger btn-lg" *ngIf="_state.backward" (click)="backward()">Effacer</button>
      <button class="btn btn-danger btn-lg" *ngIf="_state.stop" (click)="stop()">Arrêter</button>
      <button class="btn btn-primary btn-lg" *ngIf="_state.play" (click)="play()">{{_btnPlay}}</button>
    </p>
  </div>
</div>

Avec cette apparence :

img026

Le composant gère la totalité du chronomètre et de son apparence.

A la réflexion on peut se dire que l’affichage des minutes et des secondes pourrait nous resservir et que ça serait bien d’avoir un composant, donc une directive, qui le gère de façon spécifique.

En gros on voudrait pouvoir écrire le template ainsi :

<div class="container">
  <div class="text-center">
    <h1>Le chronomètre</h1>
    <hr>
    <affichage></affichage>
    <br>
    <p>
      <button class="btn btn-danger btn-lg" *ngIf="_state.backward" (click)="backward()">Effacer</button>
      <button class="btn btn-danger btn-lg" *ngIf="_state.stop" (click)="stop()">Arrêter</button>
      <button class="btn btn-primary btn-lg" *ngIf="_state.play" (click)="play()">{{_btnPlay}}</button>
    </p>
  </div>
</div>

J’ai remplacé le code pour l’affichage du temps par une directive :

<affichage></affichage>

On aura donc un nouveau template (affichage.html) avec ce code :

<h1>
  <span class="label label-primary">{{_timer.minutes}}</span> minutes
  <span class="label label-primary">{{_timer.secondes}}</span> secondes
</h1>

Il ne reste plus qu’à écrire le code du composant qui va avec ce template !

Les composants

Pour résumer la situation on va avoir deux composants (qui sont aussi des directives) avec leur template respectif avec cette architecture :

img029

Le composant AffichageComponent est un enfant du composant AppComponent. On va voir que ce n’est pas sans incidence sur le code.

Le composant AppComponent

On connaît déjà ce composant mais il va falloir un peu le modifier. Voici le nouveau code :

import { Component }    from '@angular/core';
import { TimerService } from './timer.service';
import { StateService } from './state.service';
import {AffichageComponent} from './affichage.component';

@Component({
  selector: 'chronometre',
  templateUrl: 'app/app.component.html',
  providers: [TimerService, StateService],
  directives: [AffichageComponent]
})
export class AppComponent {

  private _btnPlay: string = 'Démarrer';

  constructor(private _timer: TimerService, private _state: StateService) { }

  play() {
    this._timer.start();
    this._state.setPlay();
    this._btnPlay = 'Continuer';
  }	
  stop() {
    this._timer.stop();
    this._state.setStop();
  }
  backward() {
    this._timer.reset();
    this._state.setBackward();
    this._btnPlay = 'Démarrer';  
  }
}

Il n’y a que deux différences par rapport au code qu’on avait :

  • on importe le composant pour l’affichage :
import {AffichageComponent} from './affichage.component';
  • on déclare le composant pour l’affichage comme directive :
directives: [AffichageComponent]

Et c’est tout ! Ca permet d’utiliser le composant de l’affichage comme directive dans le template :

<affichage></affichage>

Le composant AffichageComponent

Partage du service

Voici le code de ce composant (affichage.component.ts) :

import { Component } from '@angular/core';
import { TimerService } from './timer.service';

@Component({
  selector: 'affichage',
  templateUrl: 'app/affichage.html'
})
export class AffichageComponent {
  constructor(private _timer: TimerService) { }
}

Si vous êtes observateur vous voyez qu’on utilise le même service (TimerService) que celui qu’on a créé et utilisé pour le composant de l’application (AppComponent). Pourtant ce service n’est pas déclaré ici avec un provider ! Mais comme AffichageComponent est un enfant de AppComponent le service est disponible pour lui :

img030

Le chronomètre fonctionne ainsi très bien avec ces deux composants !

Propriété Input

On était partis avec dans l’idée d’avoir un composant indépendant pour l’affichage du temps. Avec le partage du service il n’est pas sûr que cette indépendance soit vraiment assurée parce qu’on peut se retrouver dans un autre contexte où on utilise un autre service mais où on veut tout de même afficher des minutes et des secondes, pourquoi pas ?

Le partage de service n’est pas le seul moyen pour transmettre une information entre deux composants, heureusement ! Une façon simple de faire est d’utiliser des propriétés Input et Output. Dans notre cas on va avoir seulement besoin de la première parce que le composant d’affichage nécessite juste des informations en entrée.

Voici le nouveau code du composant d’affichage :

import { Component, Input } from '@angular/core';

@Component({
    selector: 'affichage',
    templateUrl: 'app/affichage.html'
})
export class AffichageComponent {
    @Input() timer;
}

On a 3 différences :

  • on importe Input d’Angular (et plus seulement Component) :
import { Component, Input } from '@angular/core';
  • on n’importe plus le service pour le timer,
  • on déclare la variable qui va recevoir l’information en entrée :
@Input() timer;

Je n’ai pas précisé le type de la variable. Pour être pur il faudrait le faire, par exemple avec un type Object. Mais pour être encore plus pur il faudrait définir une classe spécifique pour imposer le modèle d’entrée mais on se contentera de ça pour l’exemple.

Je n’ai pas mis le caractère de soulignement pour le nom de la variable parce que ça n’aurait pas trop de sens là de la considérer comme privée. Du coup il faut un peu retoucher le template (affichage.html) :

<h1>
  <span class="label label-primary">{{timer.minutes}}</span> minutes
  <span class="label label-primary">{{timer.secondes}}</span> secondes
</h1>

Il ne reste plus qu’à envoyer l’information à partir du template de composant AppComponent (app.component.html) :

<affichage [timer]="_timer"></affichage>

Et ça devrait fonctionner !

Voici un petit schéma récapitulatif :

img031

Le composant d’affichage attend un objet timer avec les deux propriétés : minutes et secondes. On l’a ainsi rendu plus générique et ainsi plus facilement réutilisable.

Mon exemple n’est évidemment pas d’un très grand réalisme mais il met bien en lumière la problématique et les moyens à notre disposition pour parvenir à nos fins.

J’avais dit au départ de cette série d’articles que Angular2 est résolument modulaire et on va le vérifier de plus en plus…

Voici les fichiers terminaux de cet article.

Laisser un commentaire