inject()Depuis la leçon 10, tes composants écrivent inject(PosteApi) sans qu'on ait dit d'où sort cet objet. Réponse : de l'injecteur d'Angular, exactement le conteneur DI que tu connais en .NET. Cette leçon transforme ce PosteApi flou en un vrai service partagé, et t'apprend à le fournir (provider) et le résoudre (inject()) au bon niveau.
input partout, ou pire, à dupliquer l'état. La DI règle ça : un service unique, injecté là où on en a besoin, source de vérité partagée. C'est aussi ta frontière de test : on remplace le vrai service par un faux d'une ligne.
Notre fil rouge a une NotePosteComponent (le champ + auto-save) et, à côté, une ListePostesComponent qui affiche tous les postes. Les deux parlent des mêmes postes. Où mettre cette liste ? Pas dans l'un des deux composants — l'autre n'y aurait pas accès. La réponse : dans un service, un objet que l'injecteur fabrique une fois et distribue aux deux.
IServiceProvider. Déclarer un service ≈ services.AddSingleton<PosteService>(). Le recevoir dans une classe ≈ l'injection par constructeur que fait le conteneur ASP.NET. Angular ajoute une nuance de taille : l'injecteur est hiérarchique et calqué sur l'arbre des composants (racine → route → composant), là où le conteneur .NET est plutôt plat avec des scopes. Même idée, arbre en plus.
@Injectable({ providedIn: 'root' })On extrait toute la logique « postes » dans une classe décorée @Injectable. Le providedIn: 'root' dit à Angular : « fabrique-en une seule instance pour toute l'application, et distribue-la à qui la demande ». C'est le singleton applicatif.
import { Injectable, signal, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Injectable({ providedIn: 'root' }) // ≈ services.AddSingleton<PosteService>()
export class PosteService {
private http = inject(HttpClient); // un service peut lui-même injecter
private _postes = signal<Poste[]>([]); // l'état VIT dans le service
readonly postes = this._postes.asReadonly(); // exposé en lecture seule
charger() {
this.http.get<Poste[]>('/api/postes')
.subscribe(list => this._postes.set(list));
}
sauverNote(id: number, texte: string) {
return this.http.put(`/api/postes/${id}/note`, { texte }); // renvoie un Observable
}
}
Remarque le motif d'encapsulation : le signal _postes est privé et modifiable, mais on n'expose que postes en lecture seule (asReadonly()). Les composants lisent, seul le service écrit. C'est le pendant exact d'une propriété C# à private set.
inject()Côté composant, on ne fabrique jamais le service à la main (new PosteService() serait une faute) : on le demande à l'injecteur avec inject(). Comme providedIn: 'root', les deux composants reçoivent la même instance — donc le même état.
// Composant A : affiche la liste
export class ListePostesComponent {
private service = inject(PosteService);
postes = this.service.postes; // signal partagé
constructor() { this.service.charger(); }
}
// Composant B : édite la note d'un poste — MÊME instance de service
export class NotePosteComponent {
private service = inject(PosteService);
enregistrer(id: number, texte: string) {
this.service.sauverNote(id, texte).subscribe();
}
}
Quand A appelle charger(), le signal postes du service se remplit — et B, qui lit le même signal, se met à jour tout seul. Aucun input, aucun événement à câbler entre A et B. C'est ça, l'état partagé par service : la réactivité des signals (leçons 06-07) traverse toute l'app via l'injecteur.
inject() vs injection par constructeur
En .NET tu écris public PosteController(IPosteService s) et le conteneur remplit. Angular sait faire pareil (constructeur), mais le style moderne est la fonction inject() appelée à l'initialisation du champ. Pourquoi : elle marche hors constructeur (dans une fonction utilitaire, un computed), joue mieux avec l'héritage, et évite le constructeur à rallonge. Contrainte : inject() ne s'appelle que dans un contexte d'injection (initialisation de champ, constructor) — pas dans un handler de clic appelé plus tard.
providedIn: 'root' couvre 90 % des cas : un singleton partagé. Mais tu peux aussi fournir un service au niveau d'un composant, via providers: [...] dans son décorateur. Là, chaque instance du composant reçoit sa propre instance du service (comme un scope lié à ce sous-arbre).
@Component({
selector: 'app-fiche-poste',
providers: [PosteService], // ⚠️ instance DÉDIÉE à ce composant et ses enfants
// ...
})
export class FichePosteComponent { /* ... */ }
providedIn: 'root' qu'on re-déclare dans providers: [] d'un composant « par réflexe ». Résultat : ce composant obtient une seconde instance, son état diverge du reste de l'app — et le partage qu'on voulait est cassé, silencieusement. Ne mets un service dans providers: [] que si tu veux vraiment une instance isolée. (2) L'injecteur remonte l'arbre : Angular cherche le service du composant vers la racine, et prend le premier trouvé. (3) Par défaut, préfère providedIn: 'root' : c'est tree-shakable (retiré du bundle si jamais injecté) et c'est le singleton attendu. (4) Un service = ta frontière de test : dans un TestBed, tu fournis un faux via providers: [{ provide: PosteService, useValue: fakeService }].
InjectionTokenUne classe est son propre token (sa clé dans l'injecteur). Mais comment injecter une URL d'API, un objet de config, une interface ? Une string ou une interface ne sont pas des clés uniques à l'exécution. On crée alors un InjectionToken explicite — l'équivalent du pattern Options / IOptions<T> de .NET.
import { InjectionToken, inject } from '@angular/core';
export const API_URL = new InjectionToken<string>('API_URL');
// fourni une fois (ex. dans la config de l'app) :
// providers: [{ provide: API_URL, useValue: 'https://api.alliance.io' }]
export class PosteService {
private url = inject(API_URL); // résolu par le token, pas par un type
}
@Injectable({ providedIn: 'root' }) est injecté par trois composants différents. Combien d'instances Angular crée-t-il, et pourquoi ?PosteService (déclaré providedIn: 'root') pour partager la liste. Un dev ajoute providers: [PosteService] sur l'un des deux. Conséquence ?string) dans plusieurs services. Pourquoi passer par un InjectionToken ?@Injectable({ providedIn: 'root' }) = un singleton applicatif (AddSingleton) : l'endroit où loge l'état partagé. On le résout avec inject() (style moderne, hors constructeur possible), jamais avec new. Fournir un service dans providers: [] d'un composant crée une instance dédiée à son sous-arbre — puissant, mais casse le partage si c'est involontaire. Pour injecter une valeur non-classe (config, URL), un InjectionToken donne la clé. Réflexe de lead : providedIn: 'root' par défaut, service = frontière de test.
Angular — Dependency injection (vue d'ensemble + providedIn), puis Hierarchical injectors pour l'arbre root → composant. Survole Providers pour useValue / useClass et l'InjectionToken.
PosteService partagé. Tu veux qu'on branche ce service sur un vrai HttpClient et qu'on voie A et B se synchroniser en direct, ou qu'on écrive le test qui remplace PosteService par un faux via providers pour sentir la frontière de test ? Demande-moi, ici, maintenant.