L’application récupère et affiche actuellement des données mockées.
Après le refactoring que vous allez opérer dans cette section, le HeroesComponent
simplifié, concentré sur la gestion de la vue.
Il sera également plus facile de le tester unitairement avec un service de mock.
1. Pourquoi des services ?
Les composants ne devraient pas récupérer ou sauvegarder directement de données, et certainement pas afficher volontairement des données mockées. Leur rôle est de présenter les données, et déléguer l’accès aux données à un service.
Dans cette section, vous allez créer un HeroService
que toutes les classes de l’application peuvent utiliser pour récupérer des héros.
Nous n’allons pas instancier ce service avec le mot-clé new
comme on instancierait un objet d’une classe lambda,
mais nous allons plutôt utiliser l’injection de dépendance d’Angular pour l’injecter dans le constructeur du HeroesComponent
.
Les services sont un excellent moyen de partager de l’information entre des classes qui ne se connaissent pas entre elles.
Vous allez créer un MessageService
et l’injecter à deux endroits.
-
Injecter dans le
HeroService
, qui utilisera le service pour envoyer un message. -
Injecter dans le
MessagesComponent
, qui affichera ce message, et affiche également l’ID quand l’utilisateur clique sur un héros.
2. Création du HeroService
Comme pour les composants, vous allez utiliser le CLI Angular pour créer le service, nommé hero
.
ng g service services/hero
La commande génère le squelette de la classe du HeroService
dans src/app/services/hero.service.ts
. Ouvrez ce fichier.
2.1. Les services @Injectable()
Vous pouvez remarquer que le nouveau service importe le symbole Injectable
d’Angular, et annote sa classe avec le décorateur @Injectable()
.
Cela permet d’indiquer que cette classe participe au système d’injection de dépendances. La classe HeroService
va fournir un service injectable, qui peut également avoir ses propres dépendances.
Pour le moment il n’a aucune dépendance, mais il en aura bientôt.
Le décorateur @Injectable()
accepte un objet de metadata pour le service, comme c’était le cas pour le décorateur @Component()
dans les classes de vos composants.
2.2. Récupérer les données de héros
Le HeroService
pourrait récupérer les données depuis n’importe où - une web API, le localStorage, ou un mock de données.
En enlevant l’accès direct aux données des composants, vous vous donnez la possibilité de changer d’avis sur l’implémentation à tout instant, sans devoir toucher à vos composants. Ils n’ont pas connaissance du fonctionnement du service.
Nous allons continuer à fournir des données de héros mockées pour le moment.
Créez une méthode getHeroes()
qui retourne le mock HEROES
créé plus tôt dans le projet. Précisez son type de retour en tableau de Hero
.
Les retours de méthodes peuvent se typer de la même manière que les attributs, comme suit:
myMethod(): myReturnType {
return myReturnValue
}
3. Mise à disposition du HeroService
Vous devez rendre le HeroService
disponible pour le système d’injection de dépendance avant qu’Angular puisse l’injecter dans le HeroesComponent
, en enregistrant un provider.
Un provider est un élément qui peut créer ou délivrer un service; dans ce cas il instancie la classe HeroService
pour mettre à disposition le service.
Maintenant, vous devez vous assurer que le HeroService
est déclaré comme provider de ce service.
Vous le déclarez avec un injector qui est responsable de choisir d’injecter le provider où il est nécessaire.
Par défaut, la commande du CLI Angular ng generate service
déclare un provider sur le root injector pour votre service dans les metadata du décorateur @Injectable()
.
Si vous regardez l’instruction @Injectable
juste au-dessus de la définition de la classe HeroService
, vous pourrez constater que la metadata providedIn
a bien pour valeur 'root'.
Quand vous "providez" le service au niveau root, Angular crée une unique instance partagée du HeroService
, et l’injecte dans toute classe qui le demande.
Déclarer le provider dans les metadata d' @Injectable()
permet également à Angular d’optimiser une application en retirant le service s’il n’est finalement pas utilisé.
Le HeroService
est désormais prêt à être connecté au HeroesComponent
.
4. Mise à jour du HeroesComponent
Ouvrez le fichier de la classe HeroesComponent
.
Supprimez l’import de HEROES
, puisque vous n’en aurez plus besoin.
Retirez l’affectation de l’attribut heroes
à HEROES
pour ne conserver que sa déclaration et son typage.
4.1. Injection du HeroService
Ajoutez un paramètre privé heroService
de type HeroService
dans le constructeur, selon la syntaxe suivante:
constructor(private myService: MyService) { }
Le paramètre définit un attribut privé heroService
et l’identifie simultanément comme point d’injection du HeroService
.
Quand Angular crée le HeroesComponent
, le système d’injection de dépendance affecte l’attribut heroService
à l’instance singleton du HeroService
.
4.2. Ajouter getHeroes()
Créez une méthode pour récupérer les héros depuis le service.
Cette méthode, qu’on appellera sobrement getHeroes
doit simplement affecter à l’attribut heroes
le retour de la méthode getHeroes
du HeroService
, selon la syntaxe suivante:
this.myAttribute = this.myService.myServiceMethod();
4.3. Appel dans ngOnInit()
Vous pourriez appeler cette méthode getHeroes
directement dans le constructeur du composant, mais ce n’est pas une bonne pratique.
De manière générale, il vaut mieux réserver le constructeur pour des initialisations simples comme relier des paramètres du constructeur à des attributs du composant. Le constructeur ne devrait rien faire. Il ne devrait surtout pas appeler une fonction qui fait un appel HTTP à un serveur distant comme un service de données le ferait.
A la place, appelez plutôt getHeroes()
dans le lifecycle hook ngOnInit() et laissez Angular appeler ngOnInit() au moment approprié après avoir construit une instance du HeroesComponent
.
ngOnInit(): void {
this.getHeroes();
}
Vous aurez également besoin d’implémenter l’interface OnInit
en modifiant la classe du composant comme ceci:
export class HeroesComponent implements OnInit {
4.4. Relax and contemplate
Une fois l’application rafraîchie dans votre navigateur, prenez le temps de la tester et de constater que celle-ci affiche toujours une liste de héros, et le détail du héros voulu à la sélection.
5. Données en Observable
La méthode HeroService.getHeroes()
a une signature synchrone, qui implique que le HeroService
peut récupérer les héros de manière synchrone (entendre avec une réponse immédiate).
Le HeroesComponent
consomme donc les résultats de getHeroes()
comme si les héros pouvaient être récupérés de manière synchrone.
this.heroes = this.heroService.getHeroes();
Dans un cas réel, ça ne fonctionnera pas. On s’en sort parce que le service renvoie des données mockées, donc accessible instantanément. Mais bientôt, l’application va récupérer des héros depuis un serveur distant, ce qui est intrinsèquement une opération asynchrone.
Le HeroService
doit attendre la réponse du serveur, donc getHeroes()
ne peut pas répondre immédiatement avec des données de héros,
et le navigateur ne va pas se figer pendant que le service attend la réponse.
HeroService.getHeroes()
doit avoir une signature asynchrone, d’une manière ou d’une autre.
Elle peut prendre une callback. Ou retourner une Promise
. Ou encore un Observable
.
Nous allons prendre le parti de retourner un Observable, notamment parce que nous utiliserons ensuite le HttpClient
Angular, qui lui, retourne des Observables.
5.1. Observable & HeroService
Observable
est une des classes clés de la librairie RxJS.
Dans la section sur HTTP que nous verrons plus tard, vous apprendrez que le HttpClient
d’Angular retourne des Observables
de RxJS.
Dans cette section, vous allez simuler la récupération de données depuis un serveur via la fonction of()
de RxJS, qui permet de créer un Observable à partir de données disponibles de manière synchrone.
Ouvrez le fichier du HeroService
, et faites évoluer la méthode getHeroes
.
1. Remplacez la valeur de retour HEROES
par of(HEROES)
.
2. Modifiez le type de retour dans la définition de la fonction par Observable<Hero[]>
.
of(HEROES)
retourne un Observable<Hero[]>
qui émet une unique valeur, le tableau de héros mockés.
Dans la section sur HTTP, vous appellerez HttpClient.get<Hero[]>() qui retourne également un Observable<Hero[]> qui émet une unique valeur, un tableau de héros depuis le body de la réponse HTTP.
|
5.2. Souscrire dans le HeroesComponent
Avant nos dernières modifications, la méthode HeroService.getHeroes
renvoyait un tableau de héros Hero[]
. Elle retourne désormais un Observable<Hero[]>
.
Vous allez devoir adapter le HeroesComponent
pour appliquer ce changement.
Retrouvez la ligne sur laquelle on assignait le résultat de getHeroes
à l’attribut this.heroes
, elle devrait ressembler à ceci:
this.heroes = this.heroService.getHeroes();
Il va vous falloir l’adapter pour souscrire à l' Observable<Hero[]>
maintenant renvoyé par le service, selon la syntaxe suivante:
this.myService.myMethod().subscribe(myResult => this.myAttribute = myResult);
Observable.subscribe()
est l’élément critique de cette modification.
La version précédente assignait un tableau de héros à l’attribut heroes
.
L’assignation se produit de manière synchrone, comme si le serveur retournait les héros instantanément ou si le navigateur pouvait geler l’écran pendant qu’il attend la réponse.
Cela ne fonctionnera pas lorsque le HeroService
fera de véritables requêtes à un serveur distant.
La nouvelle version attend que l' Observable
émette le tableau de héros (ce qui pourrait arrivait tout de suite ou dans plusieurs minutes).
Ensuite, le subscribe
passe le tableau émis à une callback (la méthode appelée une fois la réponse du serveur obtenue), qui affecte la valeur à l’attribut heroes
.
6. Afficher des messages
Dans cette section vous allez:
-
ajouter un
MessagesComponent
qui affiche les messages de l’application en bas de l’écran. -
créer un
Injectable
, le service globalMessagesService
, pour envoyer les messages à afficher. -
injecter le
MessageService
dans leHeroService
. -
afficher un message quand le
HeroService
récupère les héros avec succès.
6.1. Création du MessagesComponent
Utilisez le CLI pour créer le MessagesComponent.
Le CLI va créer les fichiers du composant dans le répertoire src/app/messages
et déclarer le MessagesComponent
dans AppModule
.
Modifiez le template d' AppComponent
pour afficher le MessagesComponent
, en ajoutant l’élément <sw-messages>
juste sous <sw-heroes>
.
Vous devriez voir apparaître le paragraphe par défaut du composant en bas de la page.
6.2. Création du MessageService
Utilisez le CLI pour créer le MessageService
dans src/app/services
.
Ouvrez le MessageService
et ajoutez-y les éléments suivants:
-
un attribut messages de type
string[]
initialisé à un tableau vide ([]
). -
une méthode add prenant pour paramètre un
message
de typestring
, qui ajoute ce message au tableauthis.messages
. -
une méthode clear qui ne prend pas de paramètre, et qui ré-assigne
this.messages
à un tableau vide.
Le service expose donc son cache de messages
et 2 méthodes, l’un qui ajoute un message au cache, et l’autre qui nettoie ce cache.
6.3. Injection dans le HeroService
Ré-ouvrez le HeroService
, et injectez-y le MessageService
dans son constructeur en tant qu’attribut private
.
Il s’agit d’un scénario de type "service-in-service": vous injectez le MessageService dans le HeroService , qui est lui-même injecté dans le HeroesComponent .
|
6.4. Envoi d’un message depuis le HeroService
Modifiez la méthode getHeroes
pour envoyer le message 'HeroService: fetched heroes' quand les héros sont récupérés.
6.5. Affichage du message depuis le HeroService
Le MessagesComponent
devrait afficher tous les messages, incluant celui envoyé par le HeroService
lorsqu’il récupère les héros.
Ouvrez le MessagesComponent
et injectez le MessageService
dans le constructeur en tant qu’attribut public
.
L’attribut messageService
doit être public parce que vous allez l’utiliser dans le template.
Angular n’accepte les bindings dans les templates que sur des attributs de composant publics. |
7. Ajout du binding au MessageService
Dans le template du MessagesComponent
, ajoutez les éléments suivants:
-
Un titre 'Messages' dans un
<h2>
. -
Un bouton de classe
clear
, affichant le texteclear
, et qui au clic exécute la méthodemessageService.clear()
. -
Une liste de div affichant les messages du
messageService
via la directivengFor
.
Il ne reste qu’à ajouter un peu de CSS sur le composant (dans messages.component.scss)
pour rendre tout ça un peu plus sexy :)
/* MessagesComponent's private CSS styles */
h2 {
color: red;
font-family: Arial, Helvetica, sans-serif;
font-weight: lighter;
}
body {
margin: 2em;
}
body, input[text], button {
color: crimson;
font-family: Cambria, Georgia;
}
button.clear {
font-family: Arial;
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
}
button:hover {
background-color: #cfd8dc;
}
button:disabled {
background-color: #eee;
color: #aaa;
cursor: auto;
}
button.clear {
color: #888;
margin-bottom: 12px;
}
Une fois la page rechargée, vous devriez voir apparaître le message du HeroService
en bas de la page, dans la zone des messages.
Cliquez sur le bouton, et il devrait disparaître (jusqu’au prochain rechargement de la page bien entendu).
Cette section est maintenant terminée, vous pouvez passer à l’étape suivante: Routing