Retourner à : Angular 18 pour débutants
Dans le dernier chapitre de notre cours dédié à Angular, nous avons vu comment utiliser les conditions et les boucles, afin de créer une liste de cartes à jouer que nous pouvons filtrer grâce à une barre de recherche. Aujourd’hui, nous allons parler de services et voir comment les utiliser pour gérer et centraliser la logique appliquée à nos données.
Dans ce nouveau chapitre, nous allons voir:
- ce que sont les services et comment créer un premier service qui nous retournera une liste de monstres
- ensuite, nous allons compléter ce service avec d’autres méthodes permettant d’ajouter, modifier, et supprimer des monstres de la liste
- pour finir, on verra comment utiliser « localStorage » pour stocker et récupérer des monstres.
Pour rappel, ce chapitre s’inscrit dans une longue lignée de posts, dont le fil rouge est la création d’une application de visualisation et gestion de cartes à collectionner de type Pokémon, Magic, Yu-Gi-Oh! ou autres.

Commençons par expliquer ce qu’est un service. Un service est une classe qui va mettre à disposition des données et/ou méthodes qui peuvent être réutilisées par plusieurs composants de notre application. Un service permet donc de séparer les données et la logique d’une application, de son affichage.
Un « Service Angular » est une classe qui est un « Singleton Injectable ». « Singleton » signifie que nous n’avons qu’une seule instance du service dans toute notre application. »Injectable » veut dire que nous pouvons injecter cette instance dans les composants où nous en avons besoin. Nous verrons dans un instant comment injecter notre service.
Si on revient à notre exemple de liste de cartes à jouer, vous voyez que dans notre fichier « app.component.ts », on déclare un tableau de monstres auquel on ajoute des monstres dans le constructeur, puis on utilise cette liste afin de générer l’affichage à l’écran.
import { Component, computed, model } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PlayingCardComponent } from './components/playing-card/playing-card.component';
import { SearchBarComponent } from './components/search-bar/search-bar.component';
import { Monster } from './models/monster.model';
import { MonsterType } from './utils/monster.utils';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, PlayingCardComponent, SearchBarComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
monsters!: Monster[];
search = model('');
filteredMonsters = computed(() => {
return this.monsters.filter(monster => monster.name.includes(this.search()));
});
constructor() {
this.monsters = [];
const monster1 = new Monster();
monster1.name = "Pik";
monster1.hp = 40;
monster1.figureCaption = "N°002 Pik";
this.monsters.push(monster1);
const monster2 = new Monster();
monster2.name = "Car";
monster2.image = "img/cara.png";
monster2.type = MonsterType.WATER;
monster2.hp = 60;
monster2.figureCaption = "N°003 Car";
this.monsters.push(monster2);
const monster3 = new Monster();
monster3.name = "Bulb";
monster3.image = "img/bulbi.png";
monster3.type = MonsterType.PLANT;
monster3.hp = 60;
monster3.figureCaption = "N°004 Bulb";
this.monsters.push(monster3);
const monster4 = new Monster();
monster4.name = "Sala";
monster4.image = "img/sala.png";
monster4.type = MonsterType.FIRE;
monster4.hp = 60;
monster4.figureCaption = "N°004 Sala";
this.monsters.push(monster4);
}
}
Cette manière de faire n’est pas top, car potentiellement on pourrait avoir besoin de cette liste de monstres dans d’autres pages. Ainsi, il est important de séparer cette gestion de données de la notre class « AppComponent ».
Avant de créer notre premier service, je propose d’apporter deux modifications à notre modèle « Monster ». Pour nous faciliter la vie, je veux lui rajouter un attribut « id » et une méthode « copy ». Pour cela, on ouvre notre fichier « monster.model.ts » et on y apporte le modifications souhaitées:
import { MonsterType } from "../utils/monster.utils";
export class Monster {
id: number = -1;
name: string = "Monster";
image: string = "img/pika.png";
type: MonsterType = MonsterType.ELECTRIC;
hp: number = 60;
figureCaption: string = "N°001 Monster";
attackName: string = "Standard Attack";
attackStrength: number = 10;
attackDescription: string = "This is an attack description...";
copy(): Monster {
return Object.assign(new Monster(), this);
}
}
Une fois que notre « model » a un identifiant et une méthode « copy », on va pouvoir créer notre service. Pour cela, je propose tout d’abord de créer un dossier qu’on va appeler “services” dans lequel on va créer un autre dossier qu’on va appeler “monster”. Ensuite, pour créer notre service, il suffit d’ouvrir un terminal et de taper:
ng generate service services/monster/monster
Cette commande va générer deux fichiers:
1) un fichier qui va contenir notre service, et
2) l’autre qui va contenir les tests associés à celui-ci.
Aujourd’hui, on va uniquement s’attarder sur le fichier qui implémente le service, mais nous reviendrons sur la partie « test » dans une autre vidéo.
Si on ouvre le fichier « monster.service.ts » qui vient d’être créé, on voit la chose suivante:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MonsterService {
constructor() { }
}
Comme vous pouvez le constater, notre service est une classe tout à fait normale avec l’annotation « @Injectable » avec le paramètre « providedIn »: ‘root’. Ce paramètre « providedIn » va indiquer la portée de notre service. Donc ici, Angular va instancier un objet « MonsterService » et le mettre à disposition de toute notre application.
Créons en vitesse une méthode qui se contentera d’afficher un console « log » afin de montrer comment utiliser un service dans un composant:
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class MonsterService {
constructor() { }
hello() {
console.log("Hello World");
}
}
Maintenant que notre méthode est implémentée, retournons dans notre fichier « app.component.ts ». On va l’injecter en créant un attribut qu’on va appeler « monsterService », qui va être égal à la méthode « inject » qu’on va importer de « @angular/core »et entre parenthèses, on va passer le service qu’on souhaite injecter dans notre composant, donc ici notre service « MonsterService ».
Nous pouvons alors utiliser les méthodes du service. On peut ainsi appeler la méthode « hello » à la fin du constructeur:
import { Component, computed, inject, model } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PlayingCardComponent } from './components/playing-card/playing-card.component';
import { SearchBarComponent } from './components/search-bar/search-bar.component';
import { Monster } from './models/monster.model';
import { MonsterType } from './utils/monster.utils';
import { MonsterService } from './services/monster/monster.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, PlayingCardComponent, SearchBarComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
monsterService = inject(MonsterService);
...
constructor() {
...
this.monsterService.hello();
}
}
Maintenant que nous savons comment créer un service et comment l’injecter dans notre composant, il est temps d’implémenter les méthodes de gestion de nos monstres.
Retournons dans notre fichier « monster.service.ts », et commençons par y créer un tableau de monstres qu’on va appeler « monsters ». Ajoutons-y les monstres que nous avons dans notre constructeur de notre composant « AppComponent », et créons également les méthodes suivantes:
- getAll()
- get(id: number)
- add(monster: Monster)
- update(monster: Monster)
- delete(id: number)
import { Injectable } from '@angular/core';
import { Monster } from '../../models/monster.model';
import { MonsterType } from '../../utils/monster.utils';
@Injectable({
providedIn: 'root'
})
export class MonsterService {
monsters: Monster[] = [];
currentId: number = 1;
constructor() {
this.monsters = [];
const monster1 = new Monster();
monster1.name = "Pik";
monster1.hp = 40;
monster1.figureCaption = "N°002 Pik";
this.monsters.push(monster1);
const monster2 = new Monster();
monster2.name = "Car";
monster2.image = "img/cara.png";
monster2.type = MonsterType.WATER;
monster2.hp = 60;
monster2.figureCaption = "N°003 Car";
this.monsters.push(monster2);
const monster3 = new Monster();
monster3.name = "Bulb";
monster3.image = "img/bulbi.png";
monster3.type = MonsterType.PLANT;
monster3.hp = 60;
monster3.figureCaption = "N°004 Bulb";
this.monsters.push(monster3);
const monster4 = new Monster();
monster4.name = "Sala";
monster4.image = "img/sala.png";
monster4.type = MonsterType.FIRE;
monster4.hp = 60;
monster4.figureCaption = "N°005 Sala";
this.monsters.push(monster4);
}
getAll() {
return this.monsters.map(monster => monster.copy());
}
get(id: number): Monster | undefined {
const monster = this.monsters.find(monster => monster.id === id)
return monster ? monster.copy() : undefined;
}
add(monster: Monster): Monster {
const monsterCopy = monster.copy()
monsterCopy.id = this.currentId;
this.monsters.push(monsterCopy.copy());
this.currentId++;
return monsterCopy;
}
update(monster: Monster): Monster {
const monsterCopy = monster.copy();
const monsterIndex = this.monsters.findIndex(monster => monster.id === monsterCopy.id);
if (monsterIndex !== -1) {
this.monsters[monsterIndex] = monsterCopy.copy();
}
return monsterCopy;
}
delete(id: number) {
const monsterIndex = this.monsters.findIndex(monster => monster.id === id);
if (monsterIndex !== -1) {
this.monsters.splice(monsterIndex, 1);
}
}
}
Adaptons maintenant notre class « AppComponent » afin d’y utiliser la méthode « getAll »:
import { CommonModule } from '@angular/common';
import { Component, computed, inject, model } from '@angular/core';
import { PlayingCardComponent } from './components/playing-card/playing-card.component';
import { Monster } from './models/monster.model';
import { SearchBarComponent } from './components/search-bar/search-bar.component';
import { MonsterService } from './services/monster/monster.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, PlayingCardComponent, SearchBarComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
monsterService = inject(MonsterService);
monsters!: Monster[];
search = model('');
filteredMonsters = computed(() => {
return this.monsters.filter(monster => monster.name.includes(this.search()));
})
constructor() {
this.monsters = this.monsterService.getAll();
}
}
Là, si on regarde notre navigateur, on voit qu’on a toujours notre liste de monstres qu’on peut filtrer grâce à la barre de recherche:

Adaptons maintenant notre service afin qu’il stocke les informations dans le stockage local de notre navigateur. Pour cela, on va utiliser l’objet « localStorage » que notre navigateur met à disposition. Ici, 2 méthodes nous intéressent:
1) la méthode « setItem » qui prend un nom de variable en paramètre ainsi que la valeur qu’on souhaite stocker, et
2) la méthode « getItem » qui prend un nom de variable et retourne la valeur stockée sous ce nom.
import { Injectable } from '@angular/core';
import { Monster } from '../../models/monster.model';
import { MonsterType } from '../../utils/monster.utils';
@Injectable({
providedIn: 'root'
})
export class MonsterService {
monsters: Monster[] = [];
currentId: number = 1;
constructor() {
this.load();
}
private save() {
localStorage.setItem('monsters', JSON.stringify(this.monsters));
}
private load() {
const monstersData = localStorage.getItem('monsters');
if (monstersData) {
this.monsters = JSON.parse(monstersData).map((monsterJson: any) => Object.assign(new Monster(), monsterJson));
this.currentId = Math.max(...this.monsters.map(monster => monster.id)) + 1;
} else {
this.init();
this.save();
}
}
private init() {
this.monsters = [];
const monster1 = new Monster();
monster1.id = this.currentId;
monster1.name = "Pik";
monster1.hp = 40;
monster1.figureCaption = "N°002 Pik";
this.monsters.push(monster1);
this.currentId++;
const monster2 = new Monster();
monster2.id = this.currentId;
monster2.name = "Car";
monster2.image = "img/cara.png";
monster2.type = MonsterType.WATER;
monster2.hp = 60;
monster2.figureCaption = "N°003 Car";
this.monsters.push(monster2);
this.currentId++;
const monster3 = new Monster();
monster3.id = this.currentId;
monster3.name = "Bulb";
monster3.image = "img/bulbi.png";
monster3.type = MonsterType.PLANT;
monster3.hp = 60;
monster3.figureCaption = "N°004 Bulb";
this.monsters.push(monster3);
this.currentId++;
const monster4 = new Monster();
monster4.id = this.currentId;
monster4.name = "Sala";
monster4.image = "img/sala.png";
monster4.type = MonsterType.FIRE;
monster4.hp = 60;
monster4.figureCaption = "N°005 Sala";
this.monsters.push(monster4);
this.currentId++;
}
getAll() {
return this.monsters.map(monster => monster.copy());
}
get(id: number): Monster | undefined {
const monster = this.monsters.find(monster => monster.id === id)
return monster ? monster.copy() : undefined;
}
add(monster: Monster): Monster {
const monsterCopy = monster.copy()
monsterCopy.id = this.currentId;
this.monsters.push(monsterCopy.copy());
this.save();
this.currentId++;
return monsterCopy;
}
update(monster: Monster): Monster {
const monsterCopy = monster.copy();
const monsterIndex = this.monsters.findIndex(monster => monster.id === monsterCopy.id);
if (monsterIndex !== -1) {
this.monsters[monsterIndex] = monsterCopy.copy();
this.save();
}
return monsterCopy;
}
delete(id: number) {
const monsterIndex = this.monsters.findIndex(monster => monster.id === id);
if (monsterIndex !== -1) {
this.monsters.splice(monsterIndex, 1);
this.save();
}
}
}
Donc ici on a créé:
- la méthode « save », qui va stocker nos monstres dans le « localStorage » sous le nom « monsters »
- la méthode « load », qui va charger les monstres stockés dans le « localStorage ». Si aucune valeur n’y est présente, on va faire appel à la méthode « init », qui va initialiser la tableau de monstres avec quatre monstres, et puis on va les stocker en faisant appel à la méthode « save ».
- Ensuite, nous avons aussi rajouté un « this.save() » à toutes les méthodes qui modifient notre tableau de monstres, donc les méthodes « add », « update » et « delete ».
Pour maintenant illustrer le résultat, ajoutez une méthode « addGenericMonster » à notre « AppComponent », qui ajoutera un monstre à notre stockage via ce service. Transformons aussi notre tableau « monsters » en signal afin que notre signal « computed » puisse être notifié à chaque fois qu’on change notre tableau de monstres:
import { CommonModule } from '@angular/common';
import { Component, computed, inject, model, signal } from '@angular/core';
import { PlayingCardComponent } from './components/playing-card/playing-card.component';
import { Monster } from './models/monster.model';
import { SearchBarComponent } from './components/search-bar/search-bar.component';
import { MonsterService } from './services/monster/monster.service';
@Component({
selector: 'app-root',
standalone: true,
imports: [CommonModule, PlayingCardComponent, SearchBarComponent],
templateUrl: './app.component.html',
styleUrl: './app.component.css'
})
export class AppComponent {
monsterService = inject(MonsterService);
monsters = signal<Monster[]>([]);
search = model('');
filteredMonsters = computed(() => {
return this.monsters().filter(monster => monster.name.includes(this.search()));
})
constructor() {
this.monsters.set(this.monsterService.getAll());
}
addGenericMonster() {
const monster = new Monster();
this.monsterService.add(monster);
this.monsters.set(this.monsterService.getAll());
}
}
Pour finir, ajoutons un bouton qui fait appel à cette fonction dans notre « app.component.html »:
<app-search-bar [(search)]="search"></app-search-bar>
<div id="card-list">
@for (monster of filteredMonsters(); track monster.id) {
<app-playing-card [monster]="monster"/>
} @empty {
<div class="centered">No monsters found !</div>
}
</div>
@if (filteredMonsters().length > 0) {
<div class="centered">Found {{filteredMonsters().length}} monsters !</div>
}
<div class="centered"><button (click)="addGenericMonster()">Add Generic Monster</button></div><br/><br/>
Si on regarde le navigateur à l’instant, on voit qu’on peut appuyer sur le bouton « Add Generic Monster » pour ajouter un monstre à la liste et qu’en rafraîchissant la page, ce nouveau monstre sera toujours présent dans le stockage local.

D’ailleurs, si vous voulez regarder le contenu du « localStorage » de votre navigateur, il suffit d’ouvrir vos outils de développement et d’aller dans l’onglet « Storage » ou « Application », selon que vous soyez sous Firefox ou Chrome. Ensuite, à gauche, sélectionnez « localStorage », et vous verrez apparaître l’URL de votre site. Si vous cliquez dessus, vous verrez un tableau avec toutes les données stockées dans le stockage local de votre navigateur pour ce site web.

Et voilà, c’est tout pour ce chapitre concernant l’introduction aux services. On en reparlera sûrement dans des chapitres plus avancés. En attendant, j’espère que ce chapitre vous a été utile.
Resources
Vous retrouvez ci-dessous un lien vers le code source utilisé dans ce chapitre. Ce lien est uniquement disponible pour les abonnés Standard et Premium.
Quiz
Répondez au quiz ci-dessous afin de vérifier que vous avez bien compris le contenu de cette leçon. Les quiz sont uniquement disponibles pour les abonnés Standard et Premium.