|
Publié le par Chloé MAHALIN

Angular 4

Créer un service singleton

Objectif

L'injection de dépendances est un procédé extrêmement pratique de la programmation objet. Elle permet de récupérer une instance, unique ou non, d'un objet sans nécessité de le communiquer tout au long de la chaîne de développement.

Avec Angular4, le mécanisme d'injection de service est disponible, et c'est tant mieux ! Il est possible d'injecter une nouvelle instance d'un objet à un composant ou alors de partager un singleton à travers l'ensemble de l'application.

Nous allons voir ensemble comment créer un singleton et l'injecter à un composant.

Définir une classe service singleton

Pour ce TP, plutôt que d'utiliser des services de requête classiques, nous allons implémenter une gestion centralisée des erreurs. L'idée est simple : dès qu'une erreur sera levée dans l'application, les messages d'erreur arriveront dans une queue, qui dispatchera les messages d'erreur à une vue spécifique selon un rythme prédéfini. La vue affichera alors les messages qu'elle reçoit au fur et à mesure qu'elle les reçoit.

Mon service !

errorQueue.ts

import { Injectable } from '@angular/core';
import { Subject } from 'rxjs/Subject';

@Injectable()
export class ErrorQueue {

  private publication: Subject<string>;
  private lastDisplayedMessage: string;

  constructor() {
    this.publication = new Subject<string>();
  }

  addMessage(message: string): void {
    this.lastDisplayedMessage = message;
    this.displayMessage(message);
  }

  displayMessage(message: string) {
    this.publication.next(message);
    setTimeout(() => {
      if(this.lastDisplayedMessage != null) {
        if (this.lastDisplayedMessage == message) {
          this.publication.next(this.createEmptyMessage());
        }
      } else {
        this.publication.next(this.createEmptyMessage());
      }
    }, 10000);
  }

  subscription(): Subject<string> {
    return this.publication;
  }

  private createEmptyMessage(): string {
    return '';
  }
}

Ici, j'ai utilisé un Subject. Son usage est défini dans un autre post, mais pour résumer, un Subject c'est un objet qui permet de gérer à la fois l'abonnement et la publication, là ou un Observable n'est disponible qu'à l'abonnement.

Dans l'exemple ci-dessus, on constate que le subject publication émet un nouveau contenu via la méthode next. Lorsque ma vue sera abonnée au subject publication, elle pourra recevoir l'ensemble des messages émis depuis la méthode next.

Que fait mon service ? Il récupère le dernier message et l'envoie aux abonnés. Au bout de 10 secondes, un message est vide est émis. Si entre-temps, un nouveau message est arrivé, il l'envoie à la suite.

Faire de mon service un singleton

Techniquement, je pourrais injecter une instance de mon service dans chacun de mes composants, mais le concept de queue ne serait alors pas fonctionnel, puisque chaque composant enverrai ses messages vers un Subject auquel personne n'est abonné.

Afin de partager ma queue, et mutualiser la gestion de mes messages d'erreur, je vais faire de ma queue un singleton :

app.module.ts

import { ErrorQueue } from './services/errorQueue';

@NgModule({
  declarations: [ AppComponent, ... ],
  imports: [ BrowserModule, FormsModule, HttpModule ],
  providers: [ ErrorQueue, MyService ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

En ajoutant ma queue directement au niveau des providers de mon @NgModule, je fait de mon service un singleton. Je délègue à Angular la tâche de dispatcher mon instance unique dans tous les composants qui en auront besoin.

Et justement...

Injecter le singleton dans un composant

Pour afficher mes messages d'erreur, j'ai besoin d'une vue. je vais la faire simple : un div qui s'affiche ou se masque si un message est vide ou non.

myErrorDisplayer.html

<div [ngClass]="{'hide': errorMessage == null || errorMessage.length == 0 }">
  <div class="message-header">
    <p>Erreur rencontrée</p>
    <button (click)='closeErrorPopup()'>close</button>
  </div>
  <div class="message-body">
    {{errorMessage}}
  </div>
</div>

myErrorDisplayer.css

.hide {
    top: -70em !important;
}
.message-header {
  color: white;
  background-color: black;
}
.message-body {
  background-color: white;
  padding: 0.8em;
}

myErrorDisplayer.ts

import { Component, OnDestroy } from '@angular/core';
import { Subscription } from 'rxjs/';

import { ErrorQueue } from '../services/errorQueue';

@Component({
  selector: 'error-displayer',
  templateUrl: './myErrorDisplayer.html',
  styleUrls: ['./myErrorDisplayer.css']
})
export class ErrorDisplayer implements OnDestroy {

  errorMessage: string;
  private queue: ErrorQueue;
  private errorSubscription: Subscription;

  constructor(queue: ErrorQueue) {
    this.queue = queue;
    this.errorSubscription = this.queue.subscription().subscribe(message => this.showMessage(message));
  }

  showMessage(message: string): void {
    this.errorMessage = message;
  }

  closeErrorPopup(): void {
    this.errorMessage = '';
  }

  ngOnDestroy() {
    this.errorSubscription.unsubscribe();
  }
}

Notre composant n'a pas besoin de déclarer un provider ErrorQueue, c'est le framework qui lui injectera directement via son constructeur lors de son instanciation. J'abonne ensuite le composant aux publications du Subject de mon service singleton via la ligne :

this.errorSubscription = this.queue.subscription().subscribe(message => this.showMessage(message));

Ma vue sera alors notifiée systématiquement dès qu'une nouvelle publication aura lieu dans ma queue de message d'erreur.

Pour terminer l'exemple

J'ai un service queue qui récupère les messages d'erreur et un composant qui les affiche. Maintenant, il ne nous reste qu'à créer un composant qui va émettre des messages d'erreur !

myErrorSender.html

<button (click)='sendNewError()'>send error</button>

myErrorSender.ts

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

import { ErrorQueue } from '../services/errorQueue';

@Component({
  selector: 'error-sender',
  templateUrl: './myErrorSender.html',
  styleUrls: ['./myErrorSender.css']
})
export class ErrorSender {

  private queue: ErrorQueue;
  private i: number;

  constructor(queue: ErrorQueue) {
    this.queue = queue;
    this.i = 0;
  }

  sendNewError(message: string): void {
    this.queue.addMessage('Here is an error message number ' + i);
    this.i = i + 1;
  }

}

Ce composant peut émettre des messages d'erreur qui seront gérés par la queue, puis envoyés au composant d'affichage, lui-même abonné aux événements de la queue.

Pour résumer

Ce qu'il faut retenir :

  • Si j'indique au niveau du NgModule, dans la directive Provider, mon service, alors Angular m'injectera la même instance de ce service dans tous les composants qui en auront besoin.
  • Si j'indique au niveau du composant, dans la directive Provider, mon service, alors Angular m'injectera une nouvelle instance de ce service.