À propos de ce cours
Un provider associe un jeton à une recette : useClass crée une instance, useValue donne une valeur, useFactory fabrique selon le contexte et useExisting crée un alias. On le déclare dans app.config.ts, dans une route, dans un composant ou dans un injecteur d’environnement, et le provider le plus proche gagne. InjectionToken représente ce qui n’est pas une classe, @Service({ factory }) place la recette sur le service et multi: true rassemble plusieurs valeurs dans un tableau. makeEnvironmentProviders() permet d’écrire ses propres fonctions provideXxx(), et provideAppInitializer() charge les données indispensables avant l’affichage. En test, overrideComponent() remplace les providers d’un composant. Le cours se termine par 7 activités corrigées et un tableau des erreurs fréquentes.
-
Objectifs
- Comprendre ce qu'est un provider: un jeton et une recette
- Savoir où configurer l'injecteur :
app.config.ts, un composant, une route, un injecteur d'environnement - Utiliser les quatre recettes :
useClass,useValue,useFactoryetuseExisting - Créer des
InjectionToken, avec ou sans valeur par défaut, et utiliser les options de@Service() - Fournir plusieurs valeurs pour un même jeton avec
multi: true - Écrire sa propre fonction
provideXxx()avecmakeEnvironmentProviders() - Exécuter du code au démarrage avec
provideAppInitializer()etprovideEnvironmentInitializer() - Créer des injecteurs d'environnement enfants et configurer l'injecteur dans les tests
-
Présentation
- Dans les chapitres précédents, on a écrit des services (
@Service()) puis on les a reçus avecinject(). Entre les deux, il y a l'injecteur: c'est lui qui sait quel objet donner pour chaque demande. - Configurer l'injecteur, c'est lui donner des providers: des instructions du type « quand on te demande
Stockage, crée unStockageMemoire». On peut ainsi changer d'implémentation, fournir une configuration, choisir selon le contexte ou préparer l'application avant son affichage, sans modifier les composants.
- Dans les chapitres précédents, on a écrit des services (
- Pour suivre ce chapitre, continuez dans le projet
premiers-pas-angularet créez un dossiersrc/app/configurer/. Tous les exemples ont été compilés et testés avec Angular 22.2 (composantsOnPushpar défaut, sans Zone.js).
-
Un provider : un jeton et une recette
- Un provider est un objet qui répond à deux questions :
provide: quel jeton est demandé ? C'est ce qu'on passe àinject(): une classe ou unInjectionToken;useClass,useValue,useFactoryouuseExisting: quelle recette utiliser pour obtenir la valeur.
- Quand on écrit
providers: [Panier], c'est une abréviation deproviders: [{ provide: Panier, useClass: Panier }]: le jeton et la classe à créer sont les mêmes.
- Un provider est un objet qui répond à deux questions :

-
Où configurer l'injecteur ?
- Angular n'a pas un seul injecteur, mais un arbre d'injecteurs. Chaque niveau peut recevoir des providers :
- la racine: le tableau
providersdeapp.config.ts, les services@Service(), les jetons avecfactory; - les injecteurs d'environnement enfants: les
providersd'une route, oucreateEnvironmentInjector(); - les injecteurs d'élément: les
providers(etviewProviders) d'un composant ou d'une directive.
- la racine: le tableau
- Quand un composant appelle
inject(X), la recherche part de son propre élément et remonte: le provider le plus proche gagne. Ici, le jetonSOURCEa une valeur par défaut, que le composantZoneremplace pour son contenu.NOM_APPLICATIONest fourni dansapp.config.ts:
- Angular n'a pas un seul injecteur, mais un arbre d'injecteurs. Chaque niveau peut recevoir des providers :
import { Component, InjectionToken, inject } from '@angular/core';
// Un jeton avec une valeur par défaut : utilisable même si personne ne le fournit
export const SOURCE = new InjectionToken<string>('SOURCE', {
factory: () => 'valeur par défaut du jeton',
});
// Fourni dans app.config.ts : { provide: NOM_APPLICATION, useValue: 'Premiers pas Angular' }
export const NOM_APPLICATION = new InjectionToken<string>('NOM_APPLICATION');
@Component({
selector: 'app-lire-source',
template: `<p>{{ etiquette }} → <strong>{{ source }}</strong></p>`,
})
export class LireSource {
protected readonly source = inject(SOURCE);
protected readonly etiquette = inject(NOM_APPLICATION);
}
@Component({
selector: 'app-zone',
providers: [{ provide: SOURCE, useValue: 'providers du composant Zone' }],
template: `<div class="zone"><ng-content /></div>`,
styles: `.zone { border: 2px dashed #3178c6; padding: 4px 12px; border-radius: 8px; }`,
})
export class Zone {}
@Component({
selector: 'app-niveaux',
imports: [LireSource, Zone],
template: `
<app-lire-source />
<app-zone>
<app-lire-source />
</app-zone>
`,
})
export class Niveaux {}
// app.config.ts (extrait)
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideHttpClient(),
{ provide: NOM_APPLICATION, useValue: 'Premiers pas Angular' },
provideSuivi({ prefixe: 'APP' }), // fonction écrite plus loin
provideParametres(), // fonction écrite plus loin
],
};

- Le premier
<app-lire-source>ne trouve aucun provider deSOURCE: la fabrique du jeton est utilisée. - Le second est dans
<app-zone>: il trouve le provider du composant avant d'atteindre la racine. - Dans un même tableau
providers, si un jeton est déclaré deux fois, c'est le dernier qui gagne.

-
useClass : choisir l'implémentation
useClassdemande à Angular de créer une instance d'une classe. Le jeton est souvent une classe abstraite qui décrit le contrat : les composants dépendent du contrat, la configuration choisit l'implémentation.- Ici, le même composant
Brouillonest utilisé deux fois, avec deux stockages différents :
import { Component, Injectable, inject, input, signal } from '@angular/core';
// Le contrat : ce que les composants demandent
export abstract class Stockage {
abstract readonly nom: string;
abstract lire(cle: string): string;
abstract ecrire(cle: string, valeur: string): void;
}
// Première recette : en mémoire (perdu au rechargement)
@Injectable()
export class StockageMemoire extends Stockage {
readonly nom = 'mémoire';
private readonly valeurs = new Map<string, string>();
lire(cle: string) { return this.valeurs.get(cle) ?? ''; }
ecrire(cle: string, valeur: string) { this.valeurs.set(cle, valeur); }
}
// Deuxième recette : dans le navigateur (conservé au rechargement)
@Injectable()
export class StockageNavigateur extends Stockage {
readonly nom = 'localStorage';
lire(cle: string) { return localStorage.getItem(cle) ?? ''; }
ecrire(cle: string, valeur: string) { localStorage.setItem(cle, valeur); }
}
@Component({
selector: 'app-brouillon',
template: `
<label>Brouillon ({{ stockage.nom }})
<input [value]="texte()" (input)="modifier($any($event.target).value)" />
</label>
`,
})
export class Brouillon {
readonly cle = input.required<string>();
protected readonly stockage = inject(Stockage);
protected readonly texte = signal('');
ngOnInit() {
this.texte.set(this.stockage.lire(this.cle()));
}
protected modifier(valeur: string) {
this.texte.set(valeur);
this.stockage.ecrire(this.cle(), valeur);
}
}
// Le même composant, deux configurations différentes
@Component({
selector: 'app-brouillon-memoire',
imports: [Brouillon],
providers: [{ provide: Stockage, useClass: StockageMemoire }],
template: `<app-brouillon cle="brouillon-a" />`,
})
export class BrouillonMemoire {}
@Component({
selector: 'app-brouillon-navigateur',
imports: [Brouillon],
providers: [{ provide: Stockage, useClass: StockageNavigateur }],
template: `<app-brouillon cle="brouillon-b" />`,
})
export class BrouillonNavigateur {}
@Component({
selector: 'app-use-class',
imports: [BrouillonMemoire, BrouillonNavigateur],
template: `
<app-brouillon-memoire />
<app-brouillon-navigateur />
`,
})
export class UseClass {}

(saisie dans les deux champs, puis rechargement de la page)
Brouillon (mémoire) : vide ← perdu
Brouillon (localStorage) : Préparer le quiz ← conservé
Brouillonne connaît niStockageMemoireniStockageNavigateur: il demandeinject(Stockage).- Une classe abstraite peut servir de jeton, car elle existe à l'exécution (contrairement à une interface). Sans provider, l'erreur est :
NG0201: No provider found for `Stockage`. - Les deux implémentations portent
@Injectable()sansprovidedIn: elles ne sont pas fournies automatiquement, seulement là où on les configure.
-
useValue et les jetons d'injection
useValuedonne une valeur déjà prête: un objet de configuration, une chaîne, un nombre, un faux service.- Pour fournir une valeur qui n'est pas une classe (une interface, une chaîne…), on crée un
InjectionToken. Son paramètre de type donne le type deinject(). Avec l'optionfactory, le jeton a une valeur par défaut à la racine, qu'un provider peut remplacer localement :
import { Component, InjectionToken, inject } from '@angular/core';
import { CurrencyPipe } from '@angular/common';
// Une interface n'existe pas à l'exécution : on crée un jeton pour la représenter
export interface ConfigEcole {
nom: string;
ville: string;
annee: string;
}
export const CONFIG_ECOLE = new InjectionToken<ConfigEcole>('CONFIG_ECOLE');
// Un jeton avec fabrique : fourni automatiquement à la racine, remplaçable
export const DEVISE = new InjectionToken<string>('DEVISE', { factory: () => 'TND' });
@Component({
selector: 'app-entete-ecole',
imports: [CurrencyPipe],
template: `
<h4>{{ config.nom }} · {{ config.ville }}</h4>
<p>Année {{ config.annee }} · Inscription : {{ 350 | currency: devise : 'symbol' : '1.0-0' }}</p>
`,
})
export class EnteteEcole {
protected readonly config = inject(CONFIG_ECOLE);
protected readonly devise = inject(DEVISE);
}
// Ce composant remplace seulement la devise pour ce qu'il contient
@Component({
selector: 'app-zone-euro',
providers: [{ provide: DEVISE, useValue: 'EUR' }],
template: `<ng-content />`,
})
export class ZoneEuro {}
@Component({
selector: 'app-jetons',
imports: [EnteteEcole, ZoneEuro],
providers: [
{
provide: CONFIG_ECOLE,
useValue: { nom: 'Centre de formation APC', ville: 'Sousse', annee: '2026-2027' },
},
],
template: `
<app-entete-ecole />
<app-zone-euro><app-entete-ecole /></app-zone-euro>
`,
})
export class Jetons {}

CONFIG_ECOLEn'a pas de fabrique : sans provider,NG0201: No provider found for `InjectionToken CONFIG_ECOLE`.DEVISEa une fabrique : sans rien configurer, il vaut'TND';ZoneEurole remplace par'EUR'pour ce qu'il contient.- La chaîne passée au constructeur (
'CONFIG_ECOLE') ne sert qu'aux messages d'erreur : deux jetons créés avec le même nom restent deux jetons différents.

-
useFactory : fabriquer selon le contexte
useFactoryconfie la création à une fonction. Elle s'exécute dans un contexte d'injection : elle peut donc appelerinject()pour lire d'autres jetons et choisir ce qu'elle renvoie.- Ici, le journal est détaillé en mode développement et discret en mode production :
import { Component, InjectionToken, Provider, inject, signal } from '@angular/core';
export type Mode = 'developpement' | 'production';
export const MODE = new InjectionToken<Mode>('MODE', { factory: () => 'developpement' });
export abstract class Journal {
abstract readonly lignes: () => readonly string[];
abstract ecrire(message: string): void;
}
class JournalDetaille extends Journal {
private readonly _lignes = signal<string[]>([]);
readonly lignes = this._lignes.asReadonly();
ecrire(message: string) {
const heure = new Date().toLocaleTimeString('fr-FR');
this._lignes.update((l) => [...l, `[${heure}] ${message}`]);
}
}
class JournalDiscret extends Journal {
private readonly nombre = signal(0);
readonly lignes = () => [`${this.nombre()} message(s) reçu(s), non affichés`];
ecrire() {
this.nombre.update((n) => n + 1);
}
}
// La fabrique choisit la recette au moment de la création
export const JOURNAL_PROVIDER: Provider = {
provide: Journal,
useFactory: () => (inject(MODE) === 'production' ? new JournalDiscret() : new JournalDetaille()),
};
@Component({
selector: 'app-console',
providers: [JOURNAL_PROVIDER],
template: `
<button (click)="journal.ecrire('Bouton cliqué')">Écrire</button>
@for (l of journal.lignes(); track $index) { <div><code>{{ l }}</code></div> }
`,
})
export class Console {
protected readonly journal = inject(Journal);
}
@Component({
selector: 'app-console-production',
imports: [Console],
providers: [{ provide: MODE, useValue: 'production' }],
template: `<app-console />`,
})
export class ConsoleProduction {}
@Component({
selector: 'app-fabrique',
imports: [Console, ConsoleProduction],
template: `
<h4>Mode développement</h4><app-console />
<h4>Mode production</h4><app-console-production />
`,
})
export class Fabrique {}

- La fabrique est appelée une seule fois par injecteur: chaque
<app-console>fournitJOURNAL_PROVIDERet a donc son propre journal. inject(MODE)dans la fabrique cherche à partir du composant : la console placée dansConsoleProductiontrouve'production'.- Les classes
JournalDetailleetJournalDiscretne sont pas exportées : seule la fabrique les connaît. - Dans du code ancien, on rencontre
useFactory: (mode: Mode) => …, deps: [MODE].depsliste les jetons passés en paramètres ; avecinject(), il n'est plus nécessaire.
-
useExisting : un alias
useExistingdit : « pour ce jeton, renvoie l'objet déjà fourni pour cet autre jeton ». C'est un alias. À ne pas confondre avecuseClass, qui crée une nouvelle instance.- Cas typique : un service a été renommé, mais d'anciens composants utilisent encore l'ancien nom :
import { Component, Injectable, inject, signal } from '@angular/core';
let compteur = 0;
@Injectable()
export class Notifications {
readonly numero = ++compteur; // numéro de l'instance
private readonly _nombre = signal(0);
readonly nombre = this._nombre.asReadonly();
envoyer() { this._nombre.update((n) => n + 1); }
}
// L'ancien nom, encore utilisé par d'anciens composants
export abstract class AncienNotifService {
abstract readonly numero: number;
abstract nombre(): number;
abstract envoyer(): void;
}
@Component({
selector: 'app-deux-noms',
template: `
<p>Notifications n° {{ nouveau.numero }} : {{ nouveau.nombre() }}
· AncienNotifService n° {{ ancien.numero }} : {{ ancien.nombre() }}</p>
<button (click)="ancien.envoyer()">Envoyer par l'ancien nom</button>
`,
})
export class DeuxNoms {
protected readonly nouveau = inject(Notifications);
protected readonly ancien = inject(AncienNotifService);
}
@Component({
selector: 'app-avec-use-existing',
imports: [DeuxNoms],
providers: [Notifications, { provide: AncienNotifService, useExisting: Notifications }],
template: `<h4>useExisting</h4><app-deux-noms />`,
})
export class AvecUseExisting {}
@Component({
selector: 'app-avec-use-class',
imports: [DeuxNoms],
providers: [Notifications, { provide: AncienNotifService, useClass: Notifications }],
template: `<h4>useClass</h4><app-deux-noms />`,
})
export class AvecUseClass {}
@Component({
selector: 'app-alias',
imports: [AvecUseExisting, AvecUseClass],
template: `<app-avec-use-existing /><app-avec-use-class />`,
})
export class Alias {}

- Avec
useExisting, les deux noms donnent l'instance n° 1: un envoi par l'ancien nom est visible des deux côtés. - Avec
useClass, l'ancien nom reçoit une instance n° 3, distincte : les compteurs divergent. - Le jeton cible doit être fourni lui aussi. Sans
Notificationsdans lesproviders:NG0201: No provider found for `Notifications`.


-
Configurer un service depuis @Service()
- Avec Angular 22, le décorateur
@Service()accepte deux options qui remplacent un provider :{ autoProvided: false }: le service n'est pas fourni automatiquement ; il faut l'ajouter à un tableauproviders(vu au chapitre sur les services) ;{ factory: () => … }: le service est fourni à la racine avec cette recette. Très pratique sur une classe abstraite, qui ne peut pas être instanciée directement.
- Avec Angular 22, le décorateur
import { Component, Service, inject } from '@angular/core';
import { DatePipe } from '@angular/common';
// Le contrat porte sa recette par défaut : aucun provider à écrire pour l'utiliser
@Service({ factory: () => new HorlogeSysteme() })
export abstract class Horloge {
abstract maintenant(): Date;
}
class HorlogeSysteme extends Horloge {
maintenant() { return new Date(); }
}
@Component({
selector: 'app-salutation',
imports: [DatePipe],
template: `<p>{{ message }} · il est {{ heure | date: 'HH:mm' }}</p>`,
})
export class Salutation {
protected readonly heure = inject(Horloge).maintenant();
protected readonly message = this.heure.getHours() < 18 ? 'Bonjour' : 'Bonsoir';
}
// Une horloge arrêtée à 20 h : pratique pour une démonstration ou un test
@Component({
selector: 'app-soiree',
imports: [Salutation],
providers: [{ provide: Horloge, useValue: { maintenant: () => new Date(2026, 10, 1, 20, 0) } }],
template: `<app-salutation />`,
})
export class Soiree {}
@Component({
selector: 'app-service-fabrique',
imports: [Salutation, Soiree],
template: `
<h4>Recette par défaut</h4><app-salutation />
<h4>Horloge remplacée</h4><app-soiree />
`,
})
export class ServiceFabrique {}

- Aucun provider n'est écrit pour la première salutation : la recette est sur
Horlogeelle-même. - Le composant
Soireeremplace la recette par un objetuseValue: c'est ce qu'on fera aussi dans un test. HorlogeSystemeest déclarée aprèsHorloge: ce n'est pas un problème, car la fabrique n'est appelée qu'à la première demande.

-
Plusieurs valeurs pour un jeton : multi: true
- Par défaut, un jeton a une valeur. Avec
multi: true, chaque provider ajoute sa valeur, etinject()renvoie un tableau. C'est le principe des « extensions » : chaque partie de l'application peut ajouter une règle sans modifier les autres. - Les comptes élèves exigent une règle de mot de passe, les comptes administrateurs trois :
- Par défaut, un jeton a une valeur. Avec
import { Component, InjectionToken, Provider, computed, inject, signal } from '@angular/core';
export interface Regle {
message: string;
verifier(valeur: string): boolean;
}
// Un jeton qui recevra PLUSIEURS valeurs : Angular les rassemble dans un tableau
export const REGLES = new InjectionToken<Regle[]>('REGLES');
export const longueur: Provider = {
provide: REGLES,
multi: true,
useValue: { message: 'au moins 8 caractères', verifier: (v: string) => v.length >= 8 },
};
export const chiffre: Provider = {
provide: REGLES,
multi: true,
useValue: { message: 'au moins un chiffre', verifier: (v: string) => /\d/.test(v) },
};
export const majuscule: Provider = {
provide: REGLES,
multi: true,
useValue: { message: 'au moins une majuscule', verifier: (v: string) => /[A-Z]/.test(v) },
};
@Component({
selector: 'app-champ-mdp',
template: `
<input type="password" placeholder="Mot de passe" (input)="valeur.set($any($event.target).value)" />
<ul>
@for (r of etat(); track r.message) {
<li [class.ok]="r.ok">{{ r.ok ? '✔' : '✘' }} {{ r.message }}</li>
}
</ul>
<p>{{ regles.length }} règle(s) · {{ valide() ? 'mot de passe accepté' : 'mot de passe refusé' }}</p>
`,
styles: `li { color: #c62828; } li.ok { color: #2e7d32; }`,
})
export class ChampMdp {
protected readonly regles = inject(REGLES);
protected readonly valeur = signal('');
protected readonly etat = computed(() =>
this.regles.map((r) => ({ message: r.message, ok: r.verifier(this.valeur()) })),
);
protected readonly valide = computed(() => this.etat().every((r) => r.ok));
}
@Component({
selector: 'app-mdp-simple',
imports: [ChampMdp],
providers: [longueur],
template: `<h4>Compte élève</h4><app-champ-mdp />`,
})
export class MdpSimple {}
@Component({
selector: 'app-mdp-renforce',
imports: [ChampMdp],
providers: [longueur, chiffre, majuscule],
template: `<h4>Compte administrateur</h4><app-champ-mdp />`,
})
export class MdpRenforce {}
@Component({
selector: 'app-multi',
imports: [MdpSimple, MdpRenforce],
template: `<app-mdp-simple /><app-mdp-renforce />`,
})
export class Multi {}

ChampMdpne connaît aucune règle : il parcourt le tableau reçu. Ajouter une règle ne demande pas de toucher au composant.- Oubli de
multi: true: aucune erreur, mais chaque provider remplace le précédent. Avec deux providersuseValue: ['8 caractères']puis['un chiffre'],inject(REGLES)renvoie seulement['un chiffre']. - Au niveau d'un composant, les multi providers d'un enfant remplacent ceux du parent, ils ne s'y ajoutent pas. Test réalisé : parent
'a', enfant'b'→ l'enfant lit['b']. C'est le piège de l'activité 5. - Angular utilise ce mécanisme lui-même : les intercepteurs HTTP, les validateurs de formulaires, les initialiseurs…

-
Écrire sa fonction provideXxx()
provideRouter(),provideHttpClient()… : Angular regroupe la configuration de chaque fonctionnalité dans une fonction. On peut faire de même avecmakeEnvironmentProviders(), qui renvoie desEnvironmentProviders.- La fonction
provideSuivi()fournit en une ligne la configuration, le service, et une fonction exécutée à la création de l'injecteur (provideEnvironmentInitializer) :
import {
EnvironmentProviders, InjectionToken, Service, inject, makeEnvironmentProviders,
provideEnvironmentInitializer, signal,
} from '@angular/core';
export interface OptionsSuivi {
prefixe: string;
}
export const OPTIONS_SUIVI = new InjectionToken<OptionsSuivi>('OPTIONS_SUIVI');
// Pas fourni automatiquement : il faut passer par provideSuivi()
@Service({ autoProvided: false })
export class Suivi {
private readonly options = inject(OPTIONS_SUIVI);
private readonly _lignes = signal<string[]>([]);
readonly lignes = this._lignes.asReadonly();
noter(message: string) {
this._lignes.update((l) => [...l, `[${this.options.prefixe}] ${message}`]);
}
}
// Une seule fonction regroupe tout ce qu'il faut fournir
export function provideSuivi(options: OptionsSuivi): EnvironmentProviders {
return makeEnvironmentProviders([
{ provide: OPTIONS_SUIVI, useValue: options },
Suivi,
provideEnvironmentInitializer(() => inject(Suivi).noter('injecteur prêt')),
]);
}
Suiviest enautoProvided: false: on ne peut l'utiliser qu'après avoir appeléprovideSuivi(), ce qui garantit queOPTIONS_SUIVIexiste.provideEnvironmentInitializer()exécute sa fonction dès que l'injecteur est créé, dans un contexte d'injection.- Les
EnvironmentProviderssont refusés dans un composant, dès la compilation :TS2322: Type 'EnvironmentProviders' is not assignable to type 'Provider'.C'est voulu : ils configurent des services globaux à une zone entière.

-
Configurer le démarrage
- Certaines données doivent être là avant le premier affichage : des paramètres, une langue, une session.
provideAppInitializer()enregistre une fonction exécutée au démarrage ; si elle renvoie unePromiseou unObservable, Angular attend sa fin avant d'afficher le premier composant. - Placer
parametres.jsondanspublic/:
- Certaines données doivent être là avant le premier affichage : des paramètres, une langue, une session.
{
"etablissement": "Centre de formation APC",
"accueil": "Bienvenue sur la plateforme des formations !",
"maxInscriptions": 3
}
import {
Component, EnvironmentProviders, Service, inject, makeEnvironmentProviders,
provideAppInitializer, signal,
} from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
export interface Reglages {
etablissement: string;
accueil: string;
maxInscriptions: number;
}
@Service()
export class Parametres {
private readonly http = inject(HttpClient);
private readonly _reglages = signal<Reglages | null>(null);
readonly reglages = this._reglages.asReadonly();
async charger() {
const debut = performance.now();
this._reglages.set(await firstValueFrom(this.http.get<Reglages>('parametres.json')));
console.log(`parametres.json chargé en ${Math.round(performance.now() - debut)} ms`);
}
}
// L'application attend la fin de charger() avant d'afficher le premier composant
export function provideParametres(): EnvironmentProviders {
return makeEnvironmentProviders([provideAppInitializer(() => inject(Parametres).charger())]);
}
@Component({
selector: 'app-accueil',
template: `
<h4>{{ reglages.etablissement }}</h4>
<p>{{ reglages.accueil }} (au plus {{ reglages.maxInscriptions }} inscriptions)</p>
`,
})
export class Accueil {
// Déjà chargés : pas de null à gérer dans le composant
protected readonly reglages = inject(Parametres).reglages()!;
}

parametres.json chargé en 48 ms
Angular is running in development mode. ← le chargement a eu lieu AVANT le démarrage
- Le composant
Accueillit les réglages dans l'initialisation d'un champ, avec!: ils ne peuvent pas êtrenull, car l'application attend la fin decharger(). - Si la promesse échoue (fichier absent, serveur arrêté), l'application ne démarre pas: page blanche et
ERROR HttpErrorResponsedans la console. Pour des données facultatives, il faut attraper l'erreur danscharger()et prévoir des valeurs par défaut. - Un initialiseur retarde l'affichage : il faut le réserver aux données vraiment indispensables.

-
Les injecteurs d'environnement enfants
- Un injecteur d'environnement enfant crée une nouvelle « zone » de services, avec ses propres instances. On en obtient un de deux façons :
- les
providersd'une route (détaillés au chapitre sur le routage) ; - la fonction
createEnvironmentInjector(providers, parent), pour afficher un composant dans une zone avec*ngComponentOutlet.
- les
- Un injecteur d'environnement enfant crée une nouvelle « zone » de services, avec ses propres instances. On en obtient un de deux façons :
import {
Component, DestroyRef, EnvironmentInjector, createEnvironmentInjector, inject,
} from '@angular/core';
import { NgComponentOutlet } from '@angular/common';
import { Suivi, provideSuivi } from './suivi';
@Component({
selector: 'app-panneau-suivi',
template: `
<button (click)="suivi.noter('clic')">Noter un clic</button>
@for (l of suivi.lignes(); track $index) { <div><code>{{ l }}</code></div> }
`,
})
export class PanneauSuivi {
protected readonly suivi = inject(Suivi);
}
@Component({
selector: 'app-environnement',
imports: [PanneauSuivi, NgComponentOutlet],
template: `
<h4>Injecteur racine (app.config.ts)</h4>
<app-panneau-suivi />
<h4>Injecteur d'environnement enfant</h4>
<ng-container *ngComponentOutlet="panneau; environmentInjector: admin" />
`,
})
export class Environnement {
protected readonly panneau = PanneauSuivi;
// Un nouvel injecteur d'environnement, enfant de celui de l'application
protected readonly admin = createEnvironmentInjector(
[provideSuivi({ prefixe: 'ADMIN' })],
inject(EnvironmentInjector),
'Administration',
);
constructor() {
inject(DestroyRef).onDestroy(() => this.admin.destroy());
}
}
![Résultat : deux Suivi indépendants, [APP] et [ADMIN]](https://apcpedagogie.com/wp-content/uploads/2026/11/angular-exemple-environnement.png)
- Le premier panneau reçoit le
Suivide la racine (provideSuivi({ prefixe: 'APP' })dansapp.config.ts), le second celui de la zoneAdministration. - L'initialiseur de
provideSuivi()s'est exécuté une fois par injecteur: on voit « injecteur prêt » dans chaque panneau. - Un injecteur créé avec
createEnvironmentInjector()doit être détruit à la main (destroy()), ici à la destruction du composant.
- Le même découpage avec des routes, vérifié par un test :
import { Routes } from '@angular/router';
import { PanneauSuivi } from './environnement';
import { provideSuivi } from './suivi';
export const routesSuivi: Routes = [
{ path: '', component: PanneauSuivi },
{
path: 'admin',
// Cette route crée son propre injecteur d'environnement
providers: [provideSuivi({ prefixe: 'ADMIN' })],
component: PanneauSuivi,
},
];
import { TestBed } from '@angular/core/testing';
import { provideRouter } from '@angular/router';
import { RouterTestingHarness } from '@angular/router/testing';
import { routesSuivi } from './routes-suivi';
import { provideSuivi } from './suivi';
describe('Providers de route', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideRouter(routesSuivi), provideSuivi({ prefixe: 'APP' })],
});
});
it('la route "" utilise le Suivi de l\'application', async () => {
const harness = await RouterTestingHarness.create('/');
expect(harness.routeNativeElement?.textContent).toContain('[APP] injecteur prêt');
});
it('la route "admin" a son propre Suivi', async () => {
const harness = await RouterTestingHarness.create('/admin');
expect(harness.routeNativeElement?.textContent).toContain('[ADMIN] injecteur prêt');
expect(harness.routeNativeElement?.textContent).not.toContain('[APP]');
});
});
$ ng test --include src/app/configurer/routes-suivi.spec.ts
✓ Providers de route > la route "" utilise le Suivi de l'application
✓ Providers de route > la route "admin" a son propre Suivi
Tests 2 passed (2)

-
Configurer l'injecteur dans un test
TestBed.configureTestingModule({ providers })configure l'injecteur racine du test. Mais un composant qui déclare ses propresproviderstrouve son service avant d'atteindre la racine : le faux service est ignoré.- Pour remplacer un provider déclaré dans un composant, on utilise
TestBed.overrideComponent():
import { Component, DestroyRef, Injectable, computed, inject, signal } from '@angular/core';
@Injectable()
export class Chrono {
private readonly _secondes = signal(0);
readonly secondes = this._secondes.asReadonly();
private id?: ReturnType<typeof setInterval>;
constructor() {
inject(DestroyRef).onDestroy(() => this.arreter());
}
demarrer() {
this.arreter();
this.id = setInterval(() => this._secondes.update((s) => s + 1), 1000);
}
arreter() { clearInterval(this.id); }
}
// Chaque minuteur fournit SON chrono
@Component({
selector: 'app-minuteur',
providers: [Chrono],
template: `
<span class="ecran">{{ affichage() }}</span>
<button (click)="chrono.demarrer()">Démarrer</button>
<button (click)="chrono.arreter()">Arrêter</button>
`,
styles: `.ecran { font: 700 1.4rem monospace; margin-right: 8px; }`,
})
export class Minuteur {
protected readonly chrono = inject(Chrono);
protected readonly affichage = computed(() => {
const s = this.chrono.secondes();
return `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`;
});
}
import { TestBed } from '@angular/core/testing';
import { signal } from '@angular/core';
import { Chrono, Minuteur } from './minuteur';
class FauxChrono {
readonly secondes = signal(75);
demarrages = 0;
demarrer() { this.demarrages++; }
arreter() {}
}
describe('Minuteur', () => {
it('les providers de TestBed ne remplacent PAS ceux du composant', async () => {
const faux = new FauxChrono();
TestBed.configureTestingModule({ providers: [{ provide: Chrono, useValue: faux }] });
const fixture = TestBed.createComponent(Minuteur);
await fixture.whenStable();
expect(fixture.debugElement.injector.get(Chrono)).not.toBe(faux);
expect(fixture.nativeElement.textContent).toContain('00:00');
});
it('overrideComponent remplace le provider du composant', async () => {
const faux = new FauxChrono();
TestBed.overrideComponent(Minuteur, {
set: { providers: [{ provide: Chrono, useValue: faux }] },
});
const fixture = TestBed.createComponent(Minuteur);
await fixture.whenStable();
expect(fixture.nativeElement.textContent).toContain('01:15');
fixture.nativeElement.querySelector('button').click();
expect(faux.demarrages).toBe(1);
});
});
$ ng test --include src/app/configurer/minuteur.spec.ts
✓ Minuteur > les providers de TestBed ne remplacent PAS ceux du composant
✓ Minuteur > overrideComponent remplace le provider du composant
Tests 2 passed (2)
- Le premier test le prouve :
fixture.debugElement.injector.get(Chrono)n'est pas le faux, et l'écran affiche00:00. overrideComponentdoit être appelé avantcreateComponent().setremplace tous les providers du composant ;addetremoveexistent aussi.- Une fonction
provideXxx()s'utilise aussi dans lesprovidersdeTestBed, commeprovideSuivi()dans le test des routes.

-
Aide-mémoire

-
Activité 1 : un pense-bête au stockage interchangeable (useClass)
- Créer la classe abstraite
Sauvegarde(description,charger(cle),enregistrer(cle, notes)) et trois implémentations :SauvegardeMemoire,SauvegardeLocale(localStorage, en JSON) etSauvegardeLimitee, qui hérite de la première et ne garde que les 3 dernières notes. - Créer le composant
PenseBete(entréetitre) : un champ qui ajoute une note avec Entrée, la liste avec un bouton ✕, et « Enregistré … » en bas. Il n'injecte queSauvegarde. - L'afficher trois fois, chacun dans un composant qui fournit une implémentation différente avec
useClass.
- Créer la classe abstraite

- Vérifier : après rechargement, seul le pense-bête permanent a gardé ses notes ; le troisième n'affiche jamais plus de 3 notes.
- Pourquoi le composant relit-il les notes avec
charger()après chaqueenregistrer()?
-
Activité 2 : des cartes de visite configurables (InjectionToken)
- Créer le jeton
ETABLISSEMENT(interface :nom,ville,telephone,couleur), sans fabrique, et le jetonLANGUE('fr' | 'en') avec la fabrique() => 'fr'. - Le composant
CarteVisiteaffiche « Bienvenue · nom » puis « ville · Téléphone : … », encadré de la couleur de l'établissement ; les libellés viennent d'un dictionnaire indexé par la langue. SiteSoussefournit seulement l'établissement ;SiteTunisfournit un autre établissement et la langue'en'.
- Créer le jeton

- Vérifier : la carte de Tunis est en anglais et en rouge, celle de Sousse en français et en bleu.
- Que se passe-t-il si l'on affiche
<app-carte-visite />hors des deux sites ? Et pour la langue ?
-
Activité 3 : un jeu de dés en mode démonstration (useFactory)
- Créer la classe abstraite
Hasard(entier(min, max)) et deux implémentations non exportées :HasardMath(Math.random()) etHasardRejoue, qui renvoie toujours la même suite de nombres, en boucle. - Créer le jeton
SUITE_DEMO(readonly number[] | null,nullpar défaut) et le providerHASARD_PROVIDER: sa fabrique choisitHasardRejouesi une suite est fournie,HasardMathsinon. - Le composant
JeuDesfournitHASARD_PROVIDER, lance deux dés à chaque clic et affiche l'historique. Une seconde partie est placée dans un composant qui fournitSUITE_DEMO=[6, 6, 3, 4, 1, 2].
- Créer la classe abstraite

- Vérifier : en mode démonstration, les lancers sont toujours 6 + 6 = 12, 3 + 4 = 7, 1 + 2 = 3, puis la suite recommence.
- Pourquoi ce mode est-il précieux pour écrire un test ?
-
Activité 4 : une vue en lecture seule du panier (useExisting)
- Créer le service
Panier(@Injectable()) : articles privés,nombreettotalcalculés,ajouter()etvider(). - Créer l'interface
PanierLecture(seulementnombreettotal) et le jetonPANIER_LECTURE. - Le composant
BoutiquefournitPanieret l'alias{ provide: PANIER_LECTURE, useExisting: Panier }.RayoninjectePanier(boutons d'ajout) ;BadgePanierinjectePANIER_LECTUREet affiche « 🛒 3 article(s) · 103 DT ».
- Créer le service

- Vérifier : le badge se met à jour à chaque ajout et revient à 0 avec « Vider ».
- Écrire
this.panier.ajouter(…)dansBadgePanier: quelle erreur donne le compilateur ? Et si l'on remplaceuseExistingparuseClass, que montre le badge ?
-
Activité 5 : des contrôles d'inscription extensibles (multi)
- Créer l'interface
Controle(verifier(candidat)renvoie un message ounull) et le jetonCONTROLES. - Écrire
provideControlesBase(), qui renvoie trois providersmulti: nom d'au moins 2 caractères, âge d'au moins 16 ans, e-mail contenant@. - Le composant
FormInscription(nom, âge, e-mail, niveau) affiche les erreurs, ou « ✔ Inscription possible », et le nombre de contrôles appliqués. - La formation « Bureautique » applique les contrôles de base ; la formation « Angular avancé » ajoute un contrôle : niveau « Confirmé » obligatoire.
- Créer l'interface

- Vérifier : 3 contrôles pour Bureautique, 4 pour Angular avancé ; « Confirmé » rend l'inscription Angular possible.
- Piège : essayer de fournir seulement le contrôle du niveau dans le composant Angular, en laissant les contrôles de base dans un composant parent. Combien de contrôles sont appliqués ? Pourquoi ?
-
Activité 6 : deux catalogues avec provideCatalogue()
- Créer le jeton privé
OPTIONS_CATALOGUE(titre,formations) et le serviceCatalogueen@Service({ autoProvided: false }): il lit les options, gère une liste de favoris et un résumé « 2 favori(s) sur 3 ». - Écrire
provideCatalogue(options)avecmakeEnvironmentProviders(): les options, le service, et unprovideEnvironmentInitializer()qui écrit « Catalogue « … » prêt (n formations) » dans la console. - Le composant
Vitrineaffiche un catalogue avec des cases à cocher.DeuxCataloguescrée deux injecteurs d'environnement (« Développement web » et « Bureautique ») et affiche une vitrine dans chacun avec*ngComponentOutlet, puis les détruit à sa destruction.
- Créer le jeton privé

- Vérifier : les favoris des deux catalogues sont indépendants ; la console affiche deux fois « … prêt ».
- Comment écrire la même chose avec deux routes
/webet/bureautique?
-
Activité 7 : tester un quiz au tirage aléatoire
- Le composant
Quizfournit le serviceTirage, qui mélange les questions au hasard, puis affiche une question à la fois, ses choix, et le score :
- Le composant
import { Component, Injectable, computed, inject, signal } from '@angular/core';
export interface Question { texte: string; choix: string[]; bonne: number; }
const QUESTIONS: Question[] = [
{ texte: 'Quelle clé dit à Angular de rassembler plusieurs valeurs ?', choix: ['multi: true', 'many: true'], bonne: 0 },
{ texte: 'Quelle recette crée un alias ?', choix: ['useClass', 'useExisting'], bonne: 1 },
{ texte: 'Où écrire provideHttpClient() ?', choix: ['app.config.ts', 'providers d\'un composant'], bonne: 0 },
];
// Mélange les questions : chaque quiz a son propre tirage
@Injectable()
export class Tirage {
melanger<T>(liste: readonly T[]): T[] {
return [...liste].sort(() => Math.random() - 0.5);
}
}
@Component({
selector: 'app-quiz',
providers: [Tirage],
template: `
@if (question(); as q) {
<p class="question">{{ rang() + 1 }}/{{ questions.length }} · {{ q.texte }}</p>
@for (c of q.choix; track $index) {
<button (click)="repondre($index)">{{ c }}</button>
}
} @else {
<p class="fin">Terminé !</p>
}
<p class="score">Score : {{ score() }}</p>
`,
})
export class Quiz {
protected readonly questions = inject(Tirage).melanger(QUESTIONS);
protected readonly rang = signal(0);
protected readonly score = signal(0);
protected readonly question = computed(() => this.questions[this.rang()]);
protected repondre(i: number) {
if (i === this.question().bonne) this.score.update((s) => s + 1);
this.rang.update((r) => r + 1);
}
}
- Écrire
quiz.spec.tsavec une classeTirageFixequi ne mélange rien et compte ses appels, et trois tests : le tirage du test est bien utilisé (« 1/3 · Quelle clé… ») ; une bonne réponse donne « Score : 1 » et passe à 2/3 ; trois bonnes réponses affichent « Terminé ! » et « Score : 3 ».

- Vérifier :
ng testaffiche 3 tests réussis, à chaque exécution. - Pourquoi
providers: [{ provide: Tirage, useValue: tirage }]dansconfigureTestingModulene suffit-il pas ?
- Les corrigés des sept activités, testés avec Angular 22, sont fournis dans l'archive de la leçon (dossier
corriges/).
-
Erreurs fréquentes
| Message ou symptôme | Cause | Solution |
|---|---|---|
NG0201: No provider found for `InjectionToken CONFIG_ECOLE`. |
Jeton sans fabrique et sans provider | Le fournir avec useValue, ou lui donner une factory |
NG0201: No provider found for `Stockage`. |
Classe abstraite demandée sans provider | { provide: Stockage, useClass: … } ou @Service({ factory }) |
NG0201: No provider found for `Notifications`. |
useExisting vers un jeton qui n'est pas fourni |
Fournir aussi le jeton cible |
TS2322: Type 'EnvironmentProviders' is not assignable to type 'Provider'. |
provideXxx() dans les providers d'un composant |
Le placer dans app.config.ts, une route ou createEnvironmentInjector() |
| Seule la dernière valeur est reçue | multi: true oublié |
Ajouter multi: true à chaque provider du jeton |
| Les règles du parent ont disparu | Multi providers dans un composant enfant : ils remplacent ceux du parent | Remettre les providers de base dans l'enfant (fonction provideXxxBase()) |
TypeError: this.s.lire is not a function |
useValue: StockageMemoire: la classe est donnée, pas une instance |
useClass: StockageMemoire |
| Deux états différents pour deux noms | useClass utilisé pour un alias |
useExisting |
Page blanche, ERROR HttpErrorResponse |
L'initialiseur de provideAppInitializer() a échoué |
Vérifier le fichier ; attraper l'erreur et prévoir des valeurs par défaut |
| Le faux service est ignoré dans le test | Le composant fournit lui-même le service | TestBed.overrideComponent(C, { set: { providers } }) |
| Fuite mémoire, initialiseur relancé | Injecteur créé avec createEnvironmentInjector() jamais détruit |
destroy() dans DestroyRef.onDestroy() |
-
À retenir
- Un provider associe un jeton (
provide) à une recette;[X]est l'abréviation de{ provide: X, useClass: X }. - On configure l'injecteur dans
app.config.ts, dans une route, dans un composant, ou aveccreateEnvironmentInjector(); le provider le plus proche gagne. useClasscrée une instance,useValuedonne une valeur,useFactoryfabrique selon le contexte,useExistingcrée un alias.InjectionToken<T>représente ce qui n'est pas une classe ; l'optionfactorylui donne une valeur par défaut.@Service({ factory })met la recette sur le service ;{ autoProvided: false }oblige à le fournir.multi: truerassemble plusieurs valeurs dans un tableau ; dans un composant enfant, elles remplacent celles du parent.makeEnvironmentProviders()permet d'écrire ses fonctionsprovideXxx(), utilisables partout sauf dans un composant.provideAppInitializer()retarde l'affichage jusqu'au chargement des données indispensables.- En test,
overrideComponentremplace les providers déclarés par un composant.
- Un provider associe un jeton (
Les composants disent ce dont ils ont besoin ; la configuration de l'injecteur décide comment y répondre. C'est elle qu'on modifie pour changer de comportement, de contexte ou d'environnement, sans réécrire l'application.