À propos de ce cours
Un provider prend cinq formes : la classe seule, useClass, useValue, useFactory ou useExisting. Les types d’Angular permettent de détecter les erreurs dès la compilation. L’instance est créée à la première demande, une fois par injecteur, et détruite avec lui. Un jeton peut fournir une fonction ou un signal ; ses dépendances se lisent dans la fabrique. Les valeurs propres à chaque environnement passent par les fichiers d’environnement et un jeton. Le modèle provideXxx(withXxx()) rend la configuration lisible et extensible ; la fonction doit vérifier ses options et lever des erreurs claires. Injector.create() assemble des classes ordinaires grâce à deps, et TestBed.inject() teste un provider sans composant. Le cours se termine par 7 activités corrigées et les erreurs fréquentes.
-
Objectifs
- Connaître les cinq formes d'un provider et leurs types TypeScript
- Savoir quand une instance fournie est créée et détruite
- Fournir autre chose qu'un service : une fonction, un signal, une valeur d'environnement
- Écrire des providers configurables sur le modèle
provideXxx(withXxx(), …) - Valider les options d'un provider pour obtenir des erreurs claires
- Créer un injecteur à la main avec
Injector.create() - Organiser et tester ses providers
-
Présentation
- Le chapitre précédent a montré où placer les providers et les quatre recettes
useClass,useValue,useFactoryetuseExisting. Celui-ci s'intéresse à leur écriture: comment définir des providers justes, bien typés, faciles à configurer et à tester. - On y apprend aussi à fournir ce qui n'est pas une classe (une fonction, un signal, une URL qui change selon l'environnement), et à construire ses propres fonctions
provideXxx(), comme celles d'Angular.
- Le chapitre précédent a montré où placer les providers et les quatre recettes
- Pour suivre ce chapitre, continuez dans le projet
premiers-pas-angularet créez un dossiersrc/app/provider/. Tous les exemples ont été compilés et testés avec Angular 22.2 (composantsOnPushpar défaut, sans Zone.js).
-
Les cinq formes d'un provider
- Un provider peut s'écrire de cinq façons. Angular fournit un type pour chacune ; toutes sont des
Provider:TypeProvider: la classe seule, forme courte de{ provide: X, useClass: X };ClassProvider:useClass, Angular crée l'instance ;ValueProvider:useValue, la valeur est donnée telle quelle ;FactoryProvider:useFactory, la fonction est appelée une fois ;ExistingProvider:useExisting, un alias vers un autre jeton.
- Un provider peut s'écrire de cinq façons. Angular fournit un type pour chacune ; toutes sont des
import {
ClassProvider, Component, ExistingProvider, FactoryProvider, Injectable, InjectionToken,
Provider, TypeProvider, ValueProvider, inject,
} from '@angular/core';
@Injectable()
export class Horaires {
readonly ouverture: string = '08:00';
}
@Injectable()
export class HorairesEte extends Horaires {
override readonly ouverture = '07:00';
}
export const SALLE = new InjectionToken<string>('SALLE');
export const CAPACITE = new InjectionToken<number>('CAPACITE');
export const RESUME = new InjectionToken<string>('RESUME');
export const LIEU = new InjectionToken<string>('LIEU');
// Les cinq formes, chacune avec son type
const forme1: TypeProvider = Horaires; // la classe seule
const forme2: ClassProvider = { provide: Horaires, useClass: HorairesEte }; // une autre classe
const forme3: ValueProvider = { provide: SALLE, useValue: 'Salle B12' }; // une valeur
const forme4: FactoryProvider = { // une fonction
provide: RESUME,
useFactory: () => `${inject(SALLE)} · ${inject(CAPACITE)} places · ouverture ${inject(Horaires).ouverture}`,
};
const forme5: ExistingProvider = { provide: LIEU, useExisting: SALLE }; // un alias
// Un tableau de providers peut contenir d'autres tableaux : Angular les aplatit
const capacite: Provider[] = [{ provide: CAPACITE, useValue: 24 }];
export const PROVIDERS_SALLE: Provider[] = [forme1, forme2, forme3, forme4, forme5, capacite];
@Component({
selector: 'app-formes',
providers: PROVIDERS_SALLE,
template: `
<p>{{ resume }}</p>
<p>Lieu (alias de SALLE) : {{ lieu }}</p>
`,
})
export class Formes {
protected readonly resume = inject(RESUME);
protected readonly lieu = inject(LIEU);
}

forme1fournitHoraires, puisforme2le redéfinit : c'est le dernier qui gagne, d'où « ouverture 07:00 ».capaciteest un tableau placé dans le tableauPROVIDERS_SALLE: Angular aplatit les tableaux imbriqués. C'est ce qui permet à une fonction de renvoyer plusieurs providers d'un coup.- Typer ses providers protège des fautes de frappe.
{ provide: Horaires, useclass: HorairesEte }est refusé :TS2353: Object literal may only specify known properties, and 'useclass' does not exist in type 'Provider'.Sansprovide:TS2322: Type '{ useClass: typeof HorairesEte; }' is not assignable to type 'Provider'.

-
Quand l'instance est-elle créée ?
- Déclarer un provider ne crée rien. Angular crée l'instance à la première demande (
inject()), la garde, et renvoie la même à chaque demande suivante dans le même injecteur. Quand cet injecteur est détruit, il appellengOnDestroy()sur les instances qu'il a créées. - Dans l'exemple, deux fenêtres fournissent
Panneau; une seule l'injecte :
- Déclarer un provider ne crée rien. Angular crée l'instance à la première demande (
import { Component, DestroyRef, Injectable, OnDestroy, inject, signal } from '@angular/core';
// Un journal partagé pour observer les créations et destructions
export const evenements = signal<string[]>([]);
const noter = (m: string) => evenements.update((l) => [...l, m]);
let numero = 0;
@Injectable()
export class Panneau implements OnDestroy {
readonly id = ++numero;
constructor() { noter(`Panneau n° ${this.id} créé`); }
// Appelée quand l'injecteur qui l'a créé est détruit
ngOnDestroy() { noter(`Panneau n° ${this.id} détruit`); }
}
@Component({
selector: 'app-fenetre',
providers: [Panneau],
template: `<span>Fenêtre</span>`,
})
export class Fenetre {
// Pas d'inject(Panneau) ici : le provider existe mais n'est pas utilisé
}
@Component({
selector: 'app-fenetre-active',
providers: [Panneau],
template: `<span>Fenêtre active (panneau n° {{ panneau.id }})</span>`,
})
export class FenetreActive {
protected readonly panneau = inject(Panneau);
constructor() {
inject(DestroyRef).onDestroy(() => noter('FenetreActive détruite'));
}
}
@Component({
selector: 'app-duree-de-vie',
imports: [Fenetre, FenetreActive],
template: `
<button (click)="visible.set(!visible())">{{ visible() ? 'Fermer' : 'Ouvrir' }} les fenêtres</button>
@if (visible()) {
<div><app-fenetre /> · <app-fenetre-active /></div>
}
<ol>@for (e of evenements(); track $index) { <li>{{ e }}</li> }</ol>
`,
})
export class DureeDeVie {
protected readonly visible = signal(false);
protected readonly evenements = evenements;
}

(Ouvrir) Panneau n° 1 créé ← un seul : Fenetre ne l'injecte pas
(Fermer) Panneau n° 1 détruit
FenetreActive détruite
(Ouvrir) Panneau n° 2 créé ← nouvel injecteur, nouvelle instance
Fenetredéclareproviders: [Panneau]mais ne l'injecte jamais : aucune instance n'est créée. Un provider inutilisé ne coûte rien.- Le provider d'un composant vit et meurt avec ce composant. À la racine, l'instance vit jusqu'à la fin de l'application ; dans un injecteur créé à la main, jusqu'à son
destroy(). - Dans une fabrique, on obtient le même nettoyage avec
inject(DestroyRef).onDestroy(): il s'exécute à la destruction de l'injecteur qui a appelé la fabrique.

-
Fournir une fonction
- Un jeton peut fournir une simple fonction: un format, un calcul, une règle. Le composant l'appelle sans savoir d'où elle vient, et on la remplace aussi facilement qu'un service :
import { Component, InjectionToken, inject } from '@angular/core';
// Le type d'une fonction de formatage
export type FormatPrix = (montant: number) => string;
// Le jeton fournit une FONCTION, avec une version par défaut
export const FORMAT_PRIX = new InjectionToken<FormatPrix>('FORMAT_PRIX', {
factory: () => (m) => `${m.toFixed(3).replace('.', ',')} DT`,
});
@Component({
selector: 'app-ticket',
template: `
@for (l of lignes; track l.nom) {
<div>{{ l.nom }} : {{ format(l.prix) }}</div>
}
<strong>Total : {{ format(total) }}</strong>
`,
})
export class Ticket {
protected readonly format = inject(FORMAT_PRIX);
protected readonly lignes = [{ nom: 'Cahier', prix: 2.5 }, { nom: 'Stylo', prix: 0.75 }];
protected readonly total = this.lignes.reduce((t, l) => t + l.prix, 0);
}
@Component({
selector: 'app-ticket-euro',
imports: [Ticket],
providers: [{
provide: FORMAT_PRIX,
useValue: (m: number) => new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(m),
}],
template: `<app-ticket />`,
})
export class TicketEuro {}
@Component({
selector: 'app-fonction',
imports: [Ticket, TicketEuro],
template: `<h4>Format par défaut</h4><app-ticket /><h4>Format remplacé</h4><app-ticket-euro />`,
})
export class Fonction {}

- Le type
FormatPrixdécrit la fonction ;InjectionToken<FormatPrix>garantit queinject(FORMAT_PRIX)renvoie bien une fonction de ce type. - Piège : appeler
inject()dans la fonction fournie. AvecuseValue: (m) => m.toLocaleString(inject(LOCALE_ID)), la fonction est appelée plus tard, par le template :NG0203: The `InjectionToken LocaleId` token injection failed. `inject()` function must be called from an injection context … - La bonne écriture lit les dépendances dans une fabrique, puis renvoie la fonction :
useFactory: () => { const l = inject(LOCALE_ID); return (m) => m.toLocaleString(l); }.

-
Fournir un signal
- La fabrique d'un jeton peut aussi renvoyer un signal. Comme le jeton est fourni à la racine, la fabrique n'est appelée qu'une fois: un seul écouteur, partagé par tous les composants.
import { Component, DOCUMENT, DestroyRef, InjectionToken, Signal, inject, signal } from '@angular/core';
// Un jeton qui fournit un SIGNAL : la largeur de la fenêtre, partagée par toute l'application
export const LARGEUR_FENETRE = new InjectionToken<Signal<number>>('LARGEUR_FENETRE', {
factory: () => {
const fenetre = inject(DOCUMENT).defaultView!;
const largeur = signal(fenetre.innerWidth);
const mesurer = () => largeur.set(fenetre.innerWidth);
fenetre.addEventListener('resize', mesurer);
// La fabrique tourne dans l'injecteur racine : ce nettoyage aura lieu à sa destruction
inject(DestroyRef).onDestroy(() => fenetre.removeEventListener('resize', mesurer));
console.log('LARGEUR_FENETRE : un seul écouteur créé');
return largeur.asReadonly();
},
});
@Component({
selector: 'app-largeur',
template: `<span>{{ largeur() }} px</span>`,
})
export class Largeur {
protected readonly largeur = inject(LARGEUR_FENETRE);
}
@Component({
selector: 'app-jeton-signal',
imports: [Largeur],
template: `
<p>En-tête : <app-largeur /></p>
<p>Menu : <app-largeur /></p>
<p>Pied de page : <app-largeur /></p>
`,
})
export class JetonSignal {}

LARGEUR_FENETRE : un seul écouteur créé ← une seule fois, pour les trois composants
(fenêtre réduite à 600 px)
En-tête : 600 px · Menu : 600 px · Pied de page : 600 px
- L'activité 2 du chapitre « Injecter un service » faisait la même chose avec une fonction
injectLargeurFenetre(), mais chaque appel ajoutait un écouteur. Avec un jeton, il n'y en a qu'un. - La fabrique renvoie
asReadonly(): les composants lisent la largeur mais ne peuvent pas la modifier. - Dans un test, on remplace le jeton par un signal fixe :
{ provide: LARGEUR_FENETRE, useValue: signal(375) }.

-
Des providers selon l'environnement
- L'adresse du serveur n'est pas la même en développement et en production. Le CLI sait choisir un fichier différent à la compilation. On crée les fichiers d'environnement avec :
$ ng generate environments
CREATE src/environments/environment.ts (31 bytes)
CREATE src/environments/environment.development.ts (31 bytes)
UPDATE angular.json (2155 bytes)
// Utilisé par ng build (configuration production)
export const environment = {
production: true,
apiUrl: 'https://api.apcpedagogie.com',
journalDetaille: false,
};
// Remplace environment.ts avec ng serve (configuration development)
export const environment = {
production: false,
apiUrl: 'http://localhost:3000',
journalDetaille: true,
};
- Puis on expose chaque valeur par un jeton :
import { Component, InjectionToken, inject } from '@angular/core';
import { environment } from '../../environments/environment';
// Les valeurs viennent du fichier d'environnement choisi à la compilation
export const API_URL = new InjectionToken<string>('API_URL', {
factory: () => environment.apiUrl,
});
export const JOURNAL_DETAILLE = new InjectionToken<boolean>('JOURNAL_DETAILLE', {
factory: () => environment.journalDetaille,
});
@Component({
selector: 'app-environnement-api',
template: `
<p>API : <code>{{ api }}</code></p>
<p>Journal détaillé : {{ detaille ? 'oui' : 'non' }}</p>
`,
})
export class EnvironnementApi {
protected readonly api = inject(API_URL);
protected readonly detaille = inject(JOURNAL_DETAILLE);
}

$ ng build
$ grep -o "https://api.apcpedagogie.com\|http://localhost:3000" dist/syntaxe-angular/browser/*.js
https://api.apcpedagogie.com ← seule l'URL de production est dans le build
angular.jsoncontient maintenant unfileReplacements: en configurationdevelopment(ng serve),environment.tsest remplacé parenvironment.development.ts.- Les composants injectent
API_URL, jamaisenvironmentdirectement : la valeur reste remplaçable dans un test ou dans une partie de l'application. - Le fichier d'environnement est compilé dans l'application: il ne doit jamais contenir de mot de passe ou de clé secrète.

-
Des providers configurables : provideXxx(withXxx())
- Angular configure ses fonctionnalités avec un modèle très lisible :
provideRouter(routes, withHashLocation()),provideHttpClient(withFetch(), withInterceptors([…])). Chaque fonctionwithXxx()apporte une option ;provideXxx()assemble le tout. - On peut écrire le même modèle pour ses propres services. Chaque fonctionnalité ajoute un objet d'options à un jeton
multi; une fabrique les fusionne avec les valeurs par défaut :
- Angular configure ses fonctionnalités avec un modèle très lisible :
import {
EnvironmentProviders, InjectionToken, Provider, Service, inject, makeEnvironmentProviders, signal,
} from '@angular/core';
export interface OptionsNotifications {
duree: number; // en millisecondes
position: 'haut' | 'bas';
maximum: number;
}
const OPTIONS_PAR_DEFAUT: OptionsNotifications = { duree: 3000, position: 'bas', maximum: 3 };
// Chaque « fonctionnalité » apporte une partie des options
const FONCTIONNALITES = new InjectionToken<Partial<OptionsNotifications>[]>('FONCTIONNALITES');
export const OPTIONS_NOTIFICATIONS = new InjectionToken<OptionsNotifications>('OPTIONS_NOTIFICATIONS');
export interface FonctionnaliteNotifications {
readonly providers: Provider[];
}
function fonctionnalite(options: Partial<OptionsNotifications>): FonctionnaliteNotifications {
return { providers: [{ provide: FONCTIONNALITES, multi: true, useValue: options }] };
}
export function withDuree(ms: number): FonctionnaliteNotifications {
if (ms < 500) {
throw new Error(`withDuree(${ms}) : la durée doit être d'au moins 500 ms.`);
}
return fonctionnalite({ duree: ms });
}
export function withPosition(position: 'haut' | 'bas'): FonctionnaliteNotifications {
return fonctionnalite({ position });
}
export function withMaximum(maximum: number): FonctionnaliteNotifications {
return fonctionnalite({ maximum });
}
// Le point d'entrée : provideNotifications(withDuree(5000), withPosition('haut'))
export function provideNotifications(...fonctionnalites: FonctionnaliteNotifications[]): EnvironmentProviders {
return makeEnvironmentProviders([
{ provide: FONCTIONNALITES, multi: true, useValue: {} }, // le tableau existe même sans fonctionnalité
...fonctionnalites.map((f) => f.providers),
{
provide: OPTIONS_NOTIFICATIONS,
useFactory: () => Object.assign({}, OPTIONS_PAR_DEFAUT, ...inject(FONCTIONNALITES)),
},
Notifications,
]);
}
@Service({ autoProvided: false })
export class Notifications {
readonly options = inject(OPTIONS_NOTIFICATIONS);
private readonly _messages = signal<string[]>([]);
readonly messages = this._messages.asReadonly();
afficher(message: string) {
// on garde au plus « maximum » messages
this._messages.update((l) => [...l, message].slice(-this.options.maximum));
setTimeout(() => this._messages.update((l) => l.filter((m) => m !== message)), this.options.duree);
}
}
import {
Component, DestroyRef, EnvironmentInjector, createEnvironmentInjector, inject, input,
} from '@angular/core';
import { NgComponentOutlet } from '@angular/common';
import { Notifications, provideNotifications, withDuree, withMaximum, withPosition } from './notifications';
@Component({
selector: 'app-boite-notifications',
template: `
<button (click)="envoyer()">Notifier</button>
<small>durée {{ notif.options.duree }} ms · position {{ notif.options.position }} · max {{ notif.options.maximum }}</small>
<div class="pile" [class.haut]="notif.options.position === 'haut'">
@for (m of notif.messages(); track m) { <div class="toast">{{ m }}</div> }
</div>
`,
styles: `.pile { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; }
.pile.haut { flex-direction: column-reverse; }
.toast { background: #1e293b; color: #fff; padding: 4px 10px; border-radius: 6px; width: fit-content; }`,
})
export class BoiteNotifications {
protected readonly notif = inject(Notifications);
private n = 0;
protected envoyer() { this.notif.afficher(`Message ${++this.n}`); }
}
// Pour la démonstration : deux configurations dans deux injecteurs d'environnement
@Component({
selector: 'app-zone-notifications',
imports: [NgComponentOutlet],
template: `
<h4>provideNotifications()</h4>
<ng-container *ngComponentOutlet="boite; environmentInjector: simple" />
<h4>provideNotifications(withDuree(8000), withPosition('haut'), withMaximum(2))</h4>
<ng-container *ngComponentOutlet="boite; environmentInjector: reglee" />
`,
})
export class ZoneNotifications {
protected readonly boite = BoiteNotifications;
private readonly parent = inject(EnvironmentInjector);
protected readonly simple = createEnvironmentInjector([provideNotifications()], this.parent);
protected readonly reglee = createEnvironmentInjector(
[provideNotifications(withDuree(8000), withPosition('haut'), withMaximum(2))],
this.parent,
);
constructor() {
inject(DestroyRef).onDestroy(() => { this.simple.destroy(); this.reglee.destroy(); });
}
}

(4 clics sur chaque bouton)
provideNotifications() Message 2 · Message 3 · Message 4 (3 au plus, en bas)
provideNotifications(withDuree(8000), withPosition('haut'), withMaximum(2))
Message 4 · Message 3 (2 au plus, en haut)
(3,4 s plus tard) la première pile est vide, la seconde reste affichée
{ provide: FONCTIONNALITES, multi: true, useValue: {} }garantit que le tableau existe même quand aucune fonctionnalité n'est passée.Object.assign()applique les options dans l'ordre : en cas de doublon (withPosition('bas'), withPosition('haut')), la dernière gagne.Notificationsest enautoProvided: false: impossible de l'utiliser sans passer parprovideNotifications(), donc sans options.- Dans l'application, l'appel se place dans
app.config.ts; l'exemple utilise deux injecteurs d'environnement pour comparer deux configurations sur la même page.

-
Valider les options
- Un jeton ne vérifie pas la valeur qu'on lui donne.
{ provide: CAPACITE, useValue: '24' }sur unInjectionToken<number>compile sans erreur, et{{ c + 1 }}affiche… « 241 ». - Une fonction
provideXxx()ouwithXxx()est l'endroit idéal pour vérifier ses paramètres et lever une erreur claire, dès le démarrage. C'est ce que faitwithDuree()plus haut :
- Un jeton ne vérifie pas la valeur qu'on lui donne.
provideNotifications(withDuree(100))
ERROR Error: withDuree(100) : la durée doit être d'au moins 500 ms.
- Le message nomme la fonction, la valeur reçue et la règle : on corrige en quelques secondes.
- Le paramètre typé (
ms: number,position: 'haut' | 'bas') élimine déjà beaucoup d'erreurs à la compilation ; la vérification couvre ce que les types ne peuvent pas dire (« au moins 500 »).

-
Créer un injecteur à la main : Injector.create()
Injector.create()construit un injecteur à partir d'une liste de providers. Il sert à assembler des classes ordinaires, sans décorateur, par exemple une bibliothèque de calcul. Comme ces classes n'ont pas de métadonnées, on indique leurs dépendances avecdeps, dans l'ordre des paramètres du constructeur :
import { Component, InjectionToken, Injector, inject, signal } from '@angular/core';
import { FORMAT_PRIX } from './fonction';
export const TAUX_REMISE = new InjectionToken<number>('TAUX_REMISE');
// Une classe ordinaire, sans décorateur
export class Caisse {
constructor(
private readonly taux: number,
private readonly format: (m: number) => string,
) {}
encaisser(montant: number) {
return `${this.format(montant)} − ${this.taux * 100} % = ${this.format(montant * (1 - this.taux))}`;
}
}
@Component({
selector: 'app-injecteur-manuel',
template: `
@for (r of resultats(); track $index) { <div>{{ r }}</div> }
<button (click)="soldes()">Caisse des soldes</button>
`,
})
export class InjecteurManuel {
private readonly injector = inject(Injector);
protected readonly resultats = signal<string[]>([]);
protected soldes() {
// Un injecteur créé à la main, enfant de celui du composant
const injecteur = Injector.create({
name: 'Soldes',
parent: this.injector,
providers: [
{ provide: TAUX_REMISE, useValue: 0.3 },
// Injector.create exige des providers « statiques » : deps liste les paramètres
{ provide: Caisse, useClass: Caisse, deps: [TAUX_REMISE, FORMAT_PRIX] },
],
});
const caisse = injecteur.get(Caisse);
this.resultats.update((l) => [...l, caisse.encaisser(40), caisse.encaisser(12.5)]);
}
}

parent: this.injectorrend visibles les providers de l'application :FORMAT_PRIXest trouvé à la racine avec sa fabrique par défaut.- Si l'on oublie
deps, TypeScript ne dit rien, mais à l'exécution :NG0204: Can't resolve all parameters for Caisse: (?, ?).Les deux?sont les deux paramètres inconnus. - Dans un composant ou un service Angular, on n'a pas besoin de
Injector.create():providersetinject()suffisent.

-
Organiser ses providers
- Dans une vraie application, chaque fonctionnalité exporte sa fonction
provideXxx(), souvent dans un fichierxxx.providers.ts, et les jetons partagés sont regroupés.app.config.tsse lit alors comme une table des matières :
- Dans une vraie application, chaque fonctionnalité exporte sa fonction

-
Tester ses providers
- Un provider se teste sans composant: on configure
TestBedavec la fonction à tester, puisTestBed.inject()lit ce qu'elle produit.
- Un provider se teste sans composant: on configure
import { TestBed } from '@angular/core/testing';
import {
Notifications, OPTIONS_NOTIFICATIONS, provideNotifications, withDuree, withMaximum, withPosition,
} from './notifications';
describe('provideNotifications', () => {
it('utilise les valeurs par défaut', () => {
TestBed.configureTestingModule({ providers: [provideNotifications()] });
expect(TestBed.inject(OPTIONS_NOTIFICATIONS)).toEqual({ duree: 3000, position: 'bas', maximum: 3 });
});
it('combine les fonctionnalités', () => {
TestBed.configureTestingModule({
providers: [provideNotifications(withPosition('haut'), withDuree(8000))],
});
expect(TestBed.inject(OPTIONS_NOTIFICATIONS)).toEqual({ duree: 8000, position: 'haut', maximum: 3 });
});
it('refuse une durée trop courte', () => {
expect(() => withDuree(100)).toThrowError("withDuree(100) : la durée doit être d'au moins 500 ms.");
});
it('garde au plus « maximum » messages', () => {
TestBed.configureTestingModule({ providers: [provideNotifications(withMaximum(2))] });
const notif = TestBed.inject(Notifications);
notif.afficher('a');
notif.afficher('b');
notif.afficher('c');
expect(notif.messages()).toEqual(['b', 'c']);
});
});
$ ng test --include src/app/provider/notifications.spec.ts
✓ provideNotifications > utilise les valeurs par défaut
✓ provideNotifications > combine les fonctionnalités
✓ provideNotifications > refuse une durée trop courte
✓ provideNotifications > garde au plus « maximum » messages
Tests 4 passed (4)

-
Aide-mémoire

-
Activité 1 : un agenda aux formats interchangeables
- Créer le type
FormatDate((d: Date) => string) et le jetonFORMAT_DATE, dont la fabrique renvoie le format long : « lundi 2 novembre 2026 ». - Créer le jeton
AUJOURDHUI(Date, fabrique :new Date()). - Le composant
Agendaaffiche trois événements avec la fonction injectée. L'afficher trois fois : format par défaut, format court (useValue: « 02/11/2026 ») et format relatif (useFactory: « demain », « dans 8 jours »), avecAUJOURDHUIfixé au 1er novembre 2026.
- Créer le type

- Vérifier : Examen Angular → « lundi 2 novembre 2026 », « 02/11/2026 », « demain » ; Vacances → « dans 49 jours ».
- Pourquoi le format relatif lit-il
AUJOURDHUIdans la fabrique et non dans la fonction renvoyée ?
-
Activité 2 : un assistant qui oublie, un assistant qui se souvient
- Créer le service
Reponses(@Injectable()) : un numéro d'instance, uneMapdes réponses, et un journal partagé qui note « Reponses n° x créé » à la construction et « … détruit » dansngOnDestroy(). - Créer le composant
Etape(entréequestion) : un champ qui lit et écrit sa réponse dansReponses. - Version 1 : chaque étape est un composant qui fournit
Reponses. Version 2 : l'assistant fournitReponsesune seule fois. Les deux affichent une étape à la fois avec@switchet deux boutons ◀ ▶.
- Créer le service

- Vérifier : taper « Amel », aller à l'étape suivante puis revenir. La version 1 a oublié la réponse (« n° 1 détruit »), la version 2 l'a gardée (une seule instance, jamais détruite).
- Où faut-il donc fournir un service dont les données doivent survivre à un composant ?
-
Activité 3 : un bandeau hors ligne
- Créer le jeton
EN_LIGNE(Signal<boolean>) : sa fabrique part denavigator.onLine, écoute les événementsonlineetofflinede la fenêtre et les retire avecDestroyRef. BandeauReseauaffiche « ⚠ Hors ligne : … » quand le réseau est coupé ;EnvoiDevoirdésactive son bouton « Envoyer le devoir » hors ligne.
- Créer le jeton

- Vérifier dans les outils de développement (onglet Réseau, « Hors ligne ») : le bandeau apparaît, le bouton se grise ; au retour du réseau, l'envoi fonctionne.
- Combien d'écouteurs
onlinesont installés si dix composants injectentEN_LIGNE?
-
Activité 4 : provideJournal(withXxx())
- Sur le modèle de
provideNotifications(), écrireprovideJournal(...fonctionnalites)et trois fonctionnalités :withNiveau('info' | 'avertissement' | 'erreur'),withHorodatage()etwithPrefixe(prefixe). Valeurs par défaut : niveau'info', pas d'heure, préfixe'APP'. withPrefixe()refuse un préfixe qui n'est pas fait de 2 à 8 lettres majuscules.- Le service
Journal(autoProvided: false) ignore les messages sous le niveau choisi et écrit « [APP] ERREUR serveur injoignable ». - Afficher deux consoles (trois boutons info / avertissement / erreur) dans deux injecteurs d'environnement :
provideJournal()etprovideJournal(withNiveau('avertissement'), withHorodatage(), withPrefixe('NOTES')).
- Sur le modèle de

- Vérifier : après un clic sur chaque bouton, le premier journal a 3 lignes, le second 2 (l'info est ignorée), avec l'heure et « [NOTES] ».
- Que donne
withPrefixe('notes')? À quel moment l'erreur apparaît-elle ?
-
Activité 5 : provideMonnaie() avec vérification
- Créer l'interface
Monnaie(code,decimales) et le jetonMONNAIE(dinar, 3 décimales par défaut). - Écrire
provideMonnaie(code, decimales = 2): elle refuse un code qui n'est pas fait de 3 lettres majuscules, et un nombre de décimales hors de 0 à 3, avec un message qui cite la valeur reçue. - Le composant
Tarifsformate deux prix avecIntl.NumberFormat. L'afficher avec la monnaie par défaut, avecprovideMonnaie('EUR')et avecprovideMonnaie('JPY', 0).
- Créer l'interface

- Vérifier : 450,000 TND, 450,00 €, 450 JPY ; et 120,5 devient 121 JPY sans décimale.
- Essayer
provideMonnaie('dinar'): lire le message. Qu'aurait affiché l'application sans cette vérification ?
-
Activité 6 : un générateur de rapports avec Injector.create()
- Créer des classes sans décorateur:
SourceNotes(trois notes d'élèves),MiseEnForme(abstraite) avecMiseEnFormeTexteetMiseEnFormeTableau, etRapport, qui reçoit par son constructeur une source, une mise en forme et un seuil, et produit les lignes puis « 2/3 admis (seuil 10) ». - Le composant
GenerateurRapporta deux boutons. Chacun crée un injecteur avecInjector.create()(le seuil vient d'un jetonSEUIL, la mise en forme dépend du bouton) puis afficheinjecteur.get(Rapport).produire().
- Créer des classes sans décorateur:

- Vérifier : « Amel a obtenu 15/20 » en texte, « | Amel | 15 | » en tableau, et « 2/3 admis (seuil 10) » dans les deux cas.
- Retirer le
depsdeRapport: quel message obtient-on, et pourquoi la compilation ne l'a-t-elle pas signalé ?
-
Activité 7 : tester provideJournal() et provideMonnaie()
- Écrire
providers.spec.ts, sans aucun composant, avecTestBed.inject():provideJournal(): les options par défaut ; trois fonctionnalités dont deuxwithPrefixe()(la dernière gagne) ; avecwithNiveau('erreur'), seul le message d'erreur est gardé ;withPrefixe('notes')lève une erreur ;provideMonnaie(): la monnaie demandée ; le dinar par défaut sans provider ; les deux erreurs de validation.
- Écrire
$ ng test --include src/app/act-provider/providers.spec.ts
✓ provideJournal > a des options par défaut
✓ provideJournal > applique les fonctionnalités dans l'ordre
✓ provideJournal > ignore les messages sous le niveau choisi
✓ provideJournal > refuse un préfixe invalide
✓ provideMonnaie > fournit la monnaie demandée
✓ provideMonnaie > garde le dinar par défaut
✓ provideMonnaie > refuse un code ou des décimales invalides
Tests 7 passed (7)
- Vérifier : 7 tests réussis.
- Pourquoi ces tests sont-ils plus rapides et plus simples que des tests de composants ?
- 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 |
|---|---|---|
TS2353: Object literal may only specify known properties, and 'useclass' does not exist in type 'Provider'. |
Faute de frappe dans une clé du provider | useClass, useValue, useFactory, useExisting |
TS2322: Type '{ useClass: … }' is not assignable to type 'Provider'. |
Clé provide oubliée |
Ajouter provide: X |
{{ c + 1 }} affiche « 241 » |
useValue: '24' sur un jeton numérique : non vérifié |
Corriger la valeur ; passer par une fonction provideXxx() typée |
NG0203: The `InjectionToken LocaleId` token injection failed … |
inject() dans la fonction fournie, appelée plus tard |
Lire la dépendance dans une fabrique, puis renvoyer la fonction |
NG0204: Can't resolve all parameters for Caisse: (?, ?). |
Injector.create() sans deps |
deps: [JETON_1, JETON_2] dans l'ordre du constructeur |
ERROR Error: withDuree(100) : la durée doit être d'au moins 500 ms. |
Option refusée par la validation | Corriger l'appel : c'est le but de la vérification |
| Mauvaise implémentation utilisée | Le même jeton est fourni deux fois : le dernier gagne | Supprimer le doublon |
| Les données disparaissent en changeant d'écran | Service fourni par un composant détruit | Le fournir plus haut (parent, route ou racine) |
| Un écouteur par composant | Fonction injectXxx() appelée dans chaque composant |
Un jeton avec fabrique à la racine |
| Une URL de développement en production | import du mauvais fichier, ou fileReplacements absent |
Importer environment.ts; vérifier angular.json |
-
À retenir
- Un provider a cinq formes : la classe seule,
useClass,useValue,useFactory,useExisting; les typesProvider,ClassProvider… évitent les fautes de frappe. - L'instance est créée à la première demande, une fois par injecteur, et détruite avec lui (
ngOnDestroy,DestroyRef). - Un jeton peut fournir une fonction ou un signal ; les dépendances se lisent dans la fabrique, jamais dans la fonction renvoyée.
- Les valeurs propres à chaque environnement passent par
ng generate environmentset un jeton avec fabrique. - Le modèle
provideXxx(withXxx())rend une configuration lisible, extensible et documentée. - Une fonction
provideXxx()doit vérifier ses options et lever une erreur claire. Injector.create()assemble des classes ordinaires grâce àdeps.TestBed.inject()teste un provider sans composant.
- Un provider a cinq formes : la classe seule,
Un bon provider se lit comme une phrase : « pour ce jeton, utilise cette recette ». Bien typé, vérifié et testé, il rend la configuration de l'application aussi fiable que son code.