| Durée | Niveau | Outils | Livrable |
|---|---|---|---|
| 6 heures (10 parties) | Débutant, après le chapitre « L'injection de dépendances et les services » | Node.js 22.18+, Angular CLI 22, VS Code, Git | Dépôt Git avec un commit par partie, un README.md et des tests qui passent |
-
Objectifs
- Organiser une application en couches: des composants qui affichent, des services qui possèdent les données et les règles, une configuration qui assemble le tout
- Créer des services avec
@Service(), garder leur état dans des signals en lecture seule, et les demander avecinject() - Faire dépendre un service d'une abstraction (classe abstraite) et choisir l'implémentation avec
useClass - Fournir des valeurs avec des
InjectionToken: valeur par défaut,useValue, une fonction remplaçable (l'horloge) - Limiter la portée d'un service à un composant avec
providers, et le recevoir dans un enfant avecinject(…, { optional: true }) - Rendre l'application extensible avec un jeton
multi, puis tout regrouper dans une fonctionprovideMediatheque(withXxx()) - Tester les services avec
TestBed, en remplaçant le stockage et la date du jour
-
Prérequis
- Avoir suivi les leçons du chapitre « L'injection de dépendances et les services » : Principes de l'injection de dépendances, Les services Angular, Injecter un service Angular, Configurer l'injecteur, Définir un provider.
- Avoir réalisé le TP 3 : réaliser une TodoList : les composants,
input(), les signals et@forsont supposés acquis.
-
Présentation du projet
- Vous allez développer la Médiathèque du lycée, l'application de prêt de livres du CDI :
- un tableau de bord: nombre de livres, livres disponibles, emprunts en cours et emprunts en retard;
- deux guichets côte à côte : chaque bibliothécaire choisit un adhérent, recherche des livres, compose sa sélection et valide l'emprunt ;
- la liste des emprunts en cours, triée par date de retour, avec un bouton « Rendre » ;
- des règles: un livre ne peut être prêté deux fois, trois livres au maximum par adhérent, un adhérent en retard est bloqué, une seule bande dessinée à la fois ;
- des notifications pour chaque prêt, retour ou refus, et une sauvegarde dans le navigateur.
- La difficulté n'est pas l'interface : c'est l'organisation. Au TP 3,
Apppossédait tout et les données voyageaient d'input en output. Ici, aucun composant ne possède de données : ce sont les services.
- Vous allez développer la Médiathèque du lycée, l'application de prêt de livres du CDI :


-
Organisation du TP
- Le TP est découpé en dix parties. Chacune indique sa durée, les notions mobilisées, les consignes, une aide, un point de contrôle à vérifier avant de continuer, et des questions.
- Répondez aux questions dans un fichier
README.md, à la racine du projet, au fur et à mesure. - À la fin de chaque partie, faites un commit:
git commit -m "Partie N : …". - Les aides donnent des squelettes de code, jamais la solution complète : c'est à vous de compléter.
- Trois règles s'appliquent à tout le TP : un composant ne contient aucune donnée métier (seulement de l'état d'affichage, comme un texte recherché) ; un service n'expose que des signals en lecture seule et des méthodes ; aucun
newd'un service : tout passe parinject().


-
Partie 0 : mise en place du projet
⏱ 20 min
Notions mobilisées : Les composants · Les services Angular
- Créez le projet sans routage, générez les cinq composants et les trois dossiers de l'architecture :
npx @angular/cli@latest new tp4-mediatheque --defaults --ssr=false --style=css --routing=false
cd tp4-mediatheque
npx ng generate component tableau-bord --skip-tests
npx ng generate component guichet --skip-tests
npx ng generate component liste-livres --skip-tests
npx ng generate component emprunts-en-cours --skip-tests
npx ng generate component zone-notifications --skip-tests
mkdir src/app/modeles src/app/services src/app/config
npx ng serve
- Dans
app.html, gardez un en-tête (<h1>Médiathèque du lycée</h1>) et un<main>vide. - Dans
styles.css, reprenez les variables de couleur et le style des boutons du TP 3, et ajoutez une classe.badgeavec trois variantes :.libre(vert),.pris(gris) et.retard(rouge). - Dans
app.config.ts, activez le français pour le pipedate:registerLocaleData(localeFr)et{ provide: LOCALE_ID, useValue: 'fr' }.
src/app/
├── modeles/ ← types et données d'exemple (partie 1) : aucun code Angular
├── services/ ← l'état et la logique : Catalogue, Adherents, Emprunts… (parties 2 à 6)
├── config/ ← jetons, règles, provideMediatheque() (parties 4 à 7)
└── tableau-bord/ guichet/ liste-livres/ emprunts-en-cours/ zone-notifications/
✅ Point de contrôle
ng servefonctionne, la page affiche le titre « Médiathèque du lycée ».- La console du navigateur (F12) ne contient aucune erreur.
❓ Questions (à répondre dans le README.md)
- Pourquoi séparer
modeles/,services/etconfig/au lieu de tout ranger à côté des composants ? - Que contient le fichier créé par
ng generate serviceavec Angular 22 ? Quel décorateur y trouve-t-on ?
git add .
git commit -m "Partie 0 : création du projet et des composants"
-
Partie 1 : le modèle et les données
⏱ 30 min
Notions mobilisées : Le typage fort
- Dans
src/app/modeles/mediatheque.ts, sans aucun code Angular :- un type
Genre('roman','bd','science','histoire') ; - quatre interfaces dont toutes les propriétés sont
readonly:Livre(id,titre,auteur,genre),Adherent(id,nom,classe),Emprunt(id,livreId,adherentId,le,retour, les deux dates au formatAAAA-MM-JJ) etReglement(maxEmprunts,dureeJours) ; - deux fonctions :
dateDuJour()etajouterJours(date, jours), qui s'appuient sur une fonction privéeformater(d: Date).
- un type
- Dans
src/app/modeles/donnees.ts: les constantesLIVRES(8 livres, dont 2 bandes dessinées) etADHERENTS(4 élèves), et une fonctionempruntsExemples(aujourdhui)qui renvoie deux emprunts datés par rapport au jour donné : l'un en retard de 6 jours, l'autre à rendre dans 11 jours.
export function ajouterJours(date: string, jours: number): string {
const d = new Date(`${date}T12:00:00`);
/* à compléter : avancer de « jours » jours, puis formater */
}
// donnees.ts
export function empruntsExemples(aujourdhui: string): Emprunt[] {
return [
{ id: 'e1', livreId: 'l2', adherentId: 'a1', le: ajouterJours(aujourdhui, -20), retour: /* en retard */ },
/* à compléter */
];
}
- Testez vos fonctions sans navigateur : Node.js 22 exécute directement un fichier TypeScript sans import.
node --no-warnings --input-type=module -e "import { ajouterJours, dateDuJour } from './src/app/modeles/mediatheque.ts'; console.log(dateDuJour(), '|', ajouterJours('2026-12-28', 7), '|', ajouterJours('2026-03-01', -1));"
2026-11-01 | 2027-01-04 | 2026-02-28
✅ Point de contrôle
ajouterJourspasse correctement d'un mois et d'une année à l'autre (résultats ci-dessus).empruntsExemples(dateDuJour())renvoie un emprunt dont la date de retour est passée, et un dont elle est à venir.
❓ Questions (à répondre dans le README.md)
- Pourquoi
ajouterJourscrée-t-il la date àT12:00:00plutôt qu'à minuit ? - Pourquoi les emprunts d'exemple sont-ils produits par une fonction qui reçoit la date du jour, et non par une constante ?
git add .
git commit -m "Partie 1 : modèle et données d'exemple"
-
Partie 2 : un premier service, le catalogue
⏱ 40 min
Notions mobilisées : Les services Angular · Injecter un service Angular · Les signals
- Générez le service :
npx ng g s services/catalogue --skip-tests. La classeCatalogue, décorée par@Service(), est disponible dans toute l'application, avec une seule instance.- un signal privé
_livres, initialisé avecLIVRES, et sa version publique en lecture seule :livres = this._livres.asReadonly(); - un
computednombre; une méthodetrouver(id); une méthoderechercher(texte)qui filtre sur le titre et l'auteur, sans tenir compte des majuscules, et renvoie tous les livres si le texte est vide.
- un signal privé
ListeLivresobtient le catalogue avecinject(Catalogue). Le texte recherché est un état d'affichage: il reste dans le composant (signalrecherche), et uncomputedlivresappellecatalogue.rechercher(). Affichez chaque livre (titre et auteur), et « Aucun livre ne correspond. » avec@empty.- Dans
App, placez temporairement la liste dans une carte « Catalogue ».
@Service()
export class Catalogue {
private readonly _livres = signal<readonly Livre[]>(LIVRES);
readonly livres = this._livres.asReadonly();
readonly nombre = computed(() => /* à compléter */);
rechercher(texte: string): readonly Livre[] { /* à compléter */ }
}
export class ListeLivres {
private readonly catalogue = inject(Catalogue);
protected readonly recherche = signal('');
protected readonly livres = computed(() => /* à compléter */);
}

✅ Point de contrôle
- Les huit livres s'affichent ; taper « hu » dans la recherche ne laisse que « Les Misérables » (Victor Hugo).
- Aucun composant ne contient le tableau des livres : il n'existe que dans
Catalogue.
@Service ne reçoit ses dépendances que par inject(). Un constructeur avec paramètre est refusé à la compilation : NG2028: @Service class cannot use constructor dependency injection. Use the `inject` function instead. Et inject() appelé dans une méthode (après la construction) donne NG0203: The `Emprunts` token injection failed. `inject()` function must be called from an injection context…❓ Questions (à répondre dans le README.md)
- Pourquoi le signal
_livresest-il privé, et exposé avecasReadonly()? Que se passerait-il si un composant pouvait appelerset()? - Si deux composants font
inject(Catalogue), reçoivent-ils le même objet ? Comment le vérifier ? - Pourquoi le texte recherché reste-t-il dans
ListeLivreset non dansCatalogue?
git add .
git commit -m "Partie 2 : service Catalogue"
-
Partie 3 : un stockage abstrait et le service des emprunts
⏱ 45 min
Notions mobilisées : Principes de l'injection de dépendances · Définir un provider · Configurer l'injecteur
- Le stockage, dans
services/stockage.ts:- une classe abstraite
Stockage, avec deux méthodes abstraites :lire<T>(cle, defaut)etecrire<T>(cle, valeur): c'est le contrat; - deux implémentations, décorées par
@Service({ autoProvided: false }):StockageLocal(danslocalStorage, clés préfixées parmediatheque., dans untry … catch) etStockageMemoire(dans uneMap, pour les tests) ; - dans
app.config.ts:{ provide: Stockage, useClass: StockageLocal }.
- une classe abstraite
- Les adhérents: un service
Adherents, sur le modèle deCatalogue(signal privé,listeen lecture seule,trouver(id)). - Les emprunts, service
Emprunts:- il demande
inject(Stockage), jamaisStockageLocal; sa liste démarre avecstockage.lire('emprunts', empruntsExemples(dateDuJour())), et uneffect, dans le constructeur, l'enregistre à chaque changement ; - méthodes :
estDisponible(livreId),estEnRetard(emprunt),emprunter(livreId, adherentId)(refusé si le livre n'est pas disponible ; retour prévu dans 14 jours) etrendre(id).
- il demande
EmpruntsEnCoursaffiche un tableau (livre, adhérent, retour prévu avecdate: 'd MMM', badge « En retard », bouton « Rendre ») à partir d'uncomputedlignesqui trie les emprunts par date de retour et les complète avec le titre et le nom.ListeLivresinjecte aussiEmpruntset affiche un badge « Disponible » ou « Emprunté ».
export abstract class Stockage {
abstract lire<T>(cle: string, defaut: T): T;
abstract ecrire<T>(cle: string, valeur: T): void;
}
@Service()
export class Emprunts {
private readonly stockage = inject(Stockage); // l'abstraction, pas l'implémentation
private readonly _liste = signal<readonly Emprunt[]>(/* à compléter */);
readonly liste = this._liste.asReadonly();
constructor() {
effect(() => /* enregistrer la liste */);
}
}

✅ Point de contrôle
- Deux emprunts s'affichent : « L'Étranger » (Amel), en rouge et « En retard », et « Les Aventures de Tintin » (Karim).
- « Rendre » sur Tintin le retire du tableau, et le catalogue le marque aussitôt « Disponible » : deux composants, un seul service.
- Après F5, l'emprunt rendu n'est pas revenu : la liste est bien enregistrée.
Stockage laisse une page vide, et la console indique : NG0201: No provider found for `Stockage`. Source: Environment Injector. Path: Emprunts -> Stockage. Une classe abstraite n'est pas un service : il faut dire à Angular quelle classe utiliser. Et useClass: Stockage est refusé : TS2322 … Cannot assign an abstract constructor type to a non-abstract constructor type.❓ Questions (à répondre dans le README.md)
- Quel est l'intérêt de faire dépendre
EmpruntsdeStockageplutôt que delocalStorage? Citez deux situations où l'on changerait d'implémentation. - Pourquoi
StockageLocalest-il décoré par@Service({ autoProvided: false })et non par@Service()? - Pourquoi l'enregistrement est-il un
effectet non un appel àecrire()dans chaque méthode ?
git add .
git commit -m "Partie 3 : stockage abstrait et emprunts"
-
Partie 4 : des jetons pour la configuration, et le tableau de bord
⏱ 40 min
Notions mobilisées : Définir un provider · Configurer l'injecteur
Empruntscontient deux valeurs « en dur » : la durée (14 jours) et la date du jour (dateDuJour()). Impossible de les changer sans modifier le service, et impossible de tester un retard. Dansconfig/jetons.ts:REGLEMENT, unInjectionToken<Reglement>dont lafactoryfournit la valeur par défaut : 3 livres, 14 jours ;HORLOGE, unInjectionToken<() => string>: une fonction qui donne la date du jour ; par défaut,dateDuJour.
- Dans
Emprunts:inject(REGLEMENT)etinject(HORLOGE); remplacez chaquedateDuJour()parthis.horloge()et14parthis.reglement.dureeJours. Ajoutez uncomputedenRetard. TableauBordaffiche quatre tuiles : livres, disponibles, emprunts, en retard (fond rouge s'il y en a). Il n'a aucune entrée: il injecteCatalogueetEmprunts.
export const HORLOGE = new InjectionToken<() => string>('HORLOGE', {
factory: () => /* à compléter */,
});
// Expérience, à retirer ensuite : dans app.config.ts
{ provide: HORLOGE, useValue: () => '2026-12-31' },

✅ Point de contrôle
- Le tableau de bord indique 8 livres, 6 disponibles, 2 emprunts, 1 en retard.
- Expérience: ajoutez le provider
HORLOGEci-dessus et rechargez. Les emprunts enregistrés n'ont pas changé, mais l'application « se croit » le 31 décembre : les deux sont en retard. Retirez ensuite ce provider.
useValue correspond au type du jeton : { provide: HORLOGE, useValue: '2026-12-31' } (une chaîne au lieu d'une fonction) compile, mais this.horloge() lève TypeError: this.horloge is not a function. ❓ Questions (à répondre dans le README.md)
- Pourquoi
HORLOGEfournit-il une fonction et non directement la date du jour (une chaîne) ? (indice : une application qui reste ouverte après minuit) TableauBordn'a aucuninput(). Comparez avec le TP 3 : qu'est-ce qui a remplacé les entrées ?- Pourquoi ne faut-il pas mettre de provider pour
REGLEMENTdansapp.config.tspour que l'application fonctionne ?
git add .
git commit -m "Partie 4 : jetons REGLEMENT et HORLOGE, tableau de bord"
-
Partie 5 : un service par guichet
⏱ 45 min
Notions mobilisées : Configurer l'injecteur · Injecter un service Angular
- Deux bibliothécaires travaillent en même temps : chacun a sa propre sélection. Générez
services/selectionet décorez-le par@Service({ autoProvided: false }): il n'existe pas à la racine.Selection: un signaladherentId(chaîne vide au départ), la liste des identifiants choisis (privée, en lecture seule),nombre,contient(id),basculer(id)etvider();Guichetdéclareproviders: [Selection]: chaque guichet crée sa propre instance. Il a une entréenumero, une liste déroulante des adhérents, une<app-liste-livres />, le résumé de la sélection et un bouton « Valider l'emprunt » (désactivé sans adhérent ou sans livre) ;valider()appelleemprunts.emprunter()pour chaque livre choisi, puis vide la sélection ;ListeLivresdemandeinject(Selection, { optional: true }): dans un guichet, elle reçoit la sélection de son guichet et affiche un bouton « + » / « ✓ » (désactivé si le livre est emprunté) ; ailleurs, elle reçoitnullet n'affiche pas de bouton.
- Dans
App, remplacez la carte « Catalogue » par deux guichets :<app-guichet [numero]="1" />et<app-guichet [numero]="2" />.

@Component({
selector: 'app-guichet',
imports: [ListeLivres],
providers: [Selection], // une instance par guichet
/* … */
})
export class Guichet {
protected readonly selection = inject(Selection);
private readonly emprunts = inject(Emprunts);
protected valider(): void { /* à compléter */ }
}

✅ Point de contrôle
- Choisir des livres au guichet 1 ne change rien au guichet 2.
- Valider au guichet 1 : les livres deviennent « Emprunté » dans les deux guichets, apparaissent dans le tableau, et le tableau de bord se met à jour.
providers: [Selection] dans Guichet, la page reste vide : NG0201: No provider found for `Selection`. Source: Environment Injector. Sans { optional: true }, la même erreur apparaît dès qu'une ListeLivres est placée hors d'un guichet.❓ Questions (à répondre dans le README.md)
- Combien d'instances de
Selection, deEmpruntset deCatalogueexistent quand la page affiche deux guichets ? - Comment
ListeLivrestrouve-t-elle la sélection de son guichet, alors qu'elle ne la reçoit pas eninput()? - Que se passerait-il si
Selectionétait décorée par@Service(), sansprovidersdansGuichet?
git add .
git commit -m "Partie 5 : guichets et sélection par composant"
-
Partie 6 : notifications et règles d'emprunt (
multi)
⏱ 45 min
Notions mobilisées : Définir un provider · Les services Angular
- Notifications: un service avec une liste de messages (
id,texte,type:'succes'ou'erreur'), des méthodessucces(),erreur()etfermer(id), chaque message disparaissant après 4 secondes.ZoneNotificationsles affiche en bas à droite. - Les règles: au lieu d'empiler des
ifdansemprunter(), chaque règle est une fonction qui reçoit uneDemandeEmpruntet renvoie un message de refus, ounull:- dans le modèle : l'interface
DemandeEmprunt(livre,adherent,disponible,enCours: les livres déjà empruntés,retards,reglement) et le typeRegleEmprunt; - dans
config/jetons.ts:REGLES_EMPRUNT, unInjectionToken<readonly RegleEmprunt[]>; - dans
config/regles.ts:regleDisponible,regleLimite(selonreglement.maxEmprunts) etregleRetard; - dans
app.config.ts: un provider par règle, avecmulti: true.
- dans le modèle : l'interface
Emprunts.emprunter()construit la demande, applique toutes les règles (regles.map(…).find(…)), notifie le premier refus, sinon crée l'emprunt et notifie « « … » prêté à … ».rendre()notifie « « … » est de retour. ».

// app.config.ts
{ provide: REGLES_EMPRUNT, useValue: regleDisponible, multi: true },
{ provide: REGLES_EMPRUNT, useValue: regleLimite, multi: true },
{ provide: REGLES_EMPRUNT, useValue: regleRetard, multi: true },
// emprunts.ts
const refus = this.regles.map((regle) => regle(demande)).find(/* à compléter */);

✅ Point de contrôle
- Un prêt à Sana affiche une notification verte. Un prêt à Amel est refusé : « Amel Ben Salah doit d'abord rendre un livre en retard. »
- Après avoir rendu « L'Étranger », Amel peut emprunter. Un quatrième livre pour le même adhérent est refusé (« … a déjà 3 livre(s) : c'est le maximum. »).
multi: true sur un seul des providers fait échouer le démarrage : Error: Cannot mix multi providers and regular providers. Et sans aucun provider, NG0201: No provider found for `InjectionToken REGLES_EMPRUNT`.❓ Questions (à répondre dans le README.md)
- Pour ajouter une règle, quels fichiers faut-il modifier ? Lequel ne change jamais?
- Pourquoi les règles reçoivent-elles une
DemandeEmprunttoute prête, au lieu d'injecter elles-mêmesEmprunts? - Pourquoi
Notificationsest-il injecté dansEmprunts, et pas appelé par le composantGuichet?
git add .
git commit -m "Partie 6 : notifications et règles multi"
-
Partie 7 : une fonction
provideMediatheque(withXxx())
⏱ 40 min
Notions mobilisées : Définir un provider · Configurer l'injecteur
app.config.tscontient maintenant quatre providers techniques. CommeprovideRouter()ouprovideHttpClient(), regroupez-les dansconfig/provide-mediatheque.ts:provideMediatheque(...fonctionnalites)renvoiemakeEnvironmentProviders([...]): d'abord les valeurs par défaut (StockageLocal, les trois règles), puis les fonctionnalités, aplaties avecflat();withReglement(choix: Partial<Reglement>)complète le règlement par défaut et vérifie les options :maxEmpruntsentier ≥ 1,dureeJoursentre 1 et 60, sinon uneErrorau message clair ;withStockage('local' | 'memoire')choisit l'implémentation avecuseClass;withRegle(regle)ajoute une règle (multi: true).
- Ajoutez dans
regles.tsune quatrième règle,regleUneBd: une seule bande dessinée à la fois.app.config.tsse réduit à :
providers: [
provideBrowserGlobalErrorListeners(),
{ provide: LOCALE_ID, useValue: 'fr' },
provideMediatheque(withReglement({ maxEmprunts: 3, dureeJours: 14 }), withRegle(regleUneBd)),
],


✅ Point de contrôle
- L'application fonctionne exactement comme avant. Sana choisit « Astérix » et « Tintin » : le premier est prêté, le second refusé (« Une seule bande dessinée à la fois pour Sana Gharbi. »).
withReglement({ maxEmprunts: 0 })empêche le démarrage, et la console affiche :withReglement : maxEmprunts doit être un entier supérieur ou égal à 1 (reçu 0).Remettez 3.
provideMediatheque() renvoie des EnvironmentProviders: ils se placent dans app.config.ts (ou les providers d'une route), jamais dans les providers d'un composant : TS2322: Type 'EnvironmentProviders' is not assignable to type 'Provider'.❓ Questions (à répondre dans le README.md)
- Dans
provideMediatheque, pourquoi les valeurs par défaut sont-elles placées avant les fonctionnalités ? - Pourquoi vérifier les options dans
withReglement, au démarrage, plutôt que lors d'un emprunt ? - Que gagne un collègue qui reprend votre projet, en lisant
app.config.ts?
git add .
git commit -m "Partie 7 : provideMediatheque"
-
Partie 8 : tester les services
⏱ 35 min
Notions mobilisées : Définir un provider · Injecter un service Angular
- L'organisation en services rend l'application testable sans afficher un seul composant. Écrivez deux fichiers de test :
services/emprunts.spec.ts: une fonctioncreer(...fonctionnalites)configureTestBedavecprovideMediatheque(withStockage('memoire'), ...)et une horloge fixe ({ provide: HORLOGE, useValue: () => '2026-11-02' }), puis renvoieTestBed.inject(Emprunts);- six tests : les deux emprunts d'exemple (dont un en retard), la date de retour selon le règlement, le refus d'un livre déjà emprunté (avec le message de la notification), la limite, le blocage d'un adhérent en retard, le retour d'un livre ;
config/provide-mediatheque.spec.ts: quatre tests sur les trois règles par défaut,withRegle,withStockageet le refus d'un règlement invalide (toThrowError).
function creer(...fonctionnalites: FonctionnaliteMediatheque[]): Emprunts {
TestBed.configureTestingModule({
providers: [
provideMediatheque(withStockage('memoire'), ...fonctionnalites),
{ provide: HORLOGE, useValue: () => '2026-11-02' },
],
});
return TestBed.inject(Emprunts);
}
it('calcule la date de retour avec le règlement', () => {
const emprunts = creer(withReglement({ dureeJours: 7 }));
expect(emprunts.emprunter('l1', 'a3')).toBe(true);
expect(emprunts.liste().at(-1)!.retour).toBe(/* à compléter */);
});

✅ Point de contrôle
npx ng test --watch=falseaffiche 10 tests réussis (ajoutez--reporters=verbosepour voir le nom de chacun).- Les tests passent quel que soit le jour où on les lance, et ne laissent rien dans
localStorage.
❓ Questions (à répondre dans le README.md)
- Sans le jeton
HORLOGE, comment écririez-vous le test « bloque un adhérent qui a un livre en retard » ? Pourquoi serait-il fragile ? - Pourquoi les tests utilisent-ils
withStockage('memoire')? - Le provider
HORLOGEest placé aprèsprovideMediatheque(). Est-ce important ici ? Pourquoi ?
git add .
git commit -m "Partie 8 : tests des services"
-
Partie 9 : variantes et finitions
⏱ 20 min
- Choisissez deux variantes (chaque binôme annonce ses choix au formateur) :
- un service
Historiquequi garde les dix derniers prêts et retours, affiché sous le tableau ; - une règle « pas d'emprunt le vendredi après 16 h », ajoutée avec
withRegle()et testée avec une horloge fixe ; - un bouton « Prolonger » (+7 jours, une seule fois), avec la règle correspondante ;
- un troisième guichet, et une option
withGuichets(n); - garder dans la sélection les livres refusés, pour que le bibliothécaire voie ce qui n'a pas été prêté ;
- une variante libre, validée par le formateur.
- un service
- Relisez votre code : aucun tableau de données dans un composant, aucun
newde service, signals publics en lecture seule, aucunconsole.log, aucune erreur dans la console.
git add .
git commit -m "Partie 9 : variantes et finitions"
-
Grille d'évaluation (sur 20)
| Critère | Points |
|---|---|
Parties 0–1 : projet organisé en dossiers, modèle typé (readonly), fonctions de date |
2 |
Partie 2 : service Catalogue (@Service(), signal privé, asReadonly()), inject() |
2 |
Partie 3 : stockage abstrait, useClass, service Emprunts, sauvegarde par effect |
3 |
Partie 4 : jetons REGLEMENT et HORLOGE, tableau de bord sans entrée |
2 |
Partie 5 : Selection fournie par Guichet, optional, deux guichets indépendants |
3 |
Partie 6 : notifications, règles avec un jeton multi |
2 |
Partie 7 : provideMediatheque(withXxx()), vérification des options |
2 |
| Partie 8 : tests des services, horloge et stockage remplacés | 2 |
| README : réponses aux questions et description des variantes | 2 |
| Total | 20 |
-
Livrables
- Le lien du dépôt Git, avec au moins dix commits nommés « Partie N : … ».
- Le fichier
README.md: vos noms, les réponses aux questions, les deux variantes choisies, une capture d'écran de l'application et une du résultat des tests. - Le projet doit se lancer avec
npm installpuisnpx ng serve, etnpx ng test --watch=falsedoit réussir.
-
Erreurs fréquentes
| Message ou symptôme | Cause | Solution |
|---|---|---|
NG2028: @Service class cannot use constructor dependency injection. Use the `inject` function instead. |
Dépendance passée au constructeur d'un @Service |
private readonly x = inject(X); |
NG0203: The `Emprunts` token injection failed. `inject()` function must be called from an injection context… |
inject() appelé dans une méthode |
L'appeler dans un champ de la classe |
NG0201: No provider found for `Stockage`. |
Classe abstraite sans provider | { provide: Stockage, useClass: StockageLocal } ou provideMediatheque() |
TS2322 … Cannot assign an abstract constructor type to a non-abstract constructor type. |
useClass: Stockage |
Donner une implémentation concrète |
NG0201: No provider found for `Selection`. |
providers: [Selection] oublié, ou ListeLivres hors d'un guichet |
Ajouter le provider ; inject(Selection, { optional: true }) |
Error: Cannot mix multi providers and regular providers |
multi: true oublié sur une règle |
Le mettre sur tous les providers du jeton |
TS2322: Type 'EnvironmentProviders' is not assignable to type 'Provider'. |
provideMediatheque() dans un composant |
Le placer dans app.config.ts |
TS2339: Property 'push' does not exist on type 'readonly string[]'. |
Modification d'un signal exposé en lecture seule | Passer par une méthode du service (basculer()) |
withReglement : maxEmprunts doit être un entier supérieur ou égal à 1 (reçu 0)., page blanche |
Option refusée par la vérification | Corriger la valeur dans app.config.ts |
| Les deux guichets partagent la même sélection | Selection fournie à la racine (@Service()) |
autoProvided: false et providers dans Guichet |
| Un test passe aujourd'hui et échoue demain | Date du jour lue directement | Injecter HORLOGE et la fixer dans le test |
| Les tests voient les emprunts d'une autre exécution | StockageLocal utilisé dans les tests |
withStockage('memoire') |
-
Pour aller plus loin
- Charger le catalogue depuis une API avec
HttpClient: seulCataloguechange, les composants ne voient rien. - Afficher les guichets, le catalogue et les statistiques sur des pages différentes, avec le routage: c'est le chapitre suivant.
- Fournir un règlement différent par page avec les
providersd'une route.
- Charger le catalogue depuis une API avec
-
Bilan
- Les composants affichent, les services possèdent les données et les règles, la configuration assemble: chaque couche peut changer sans toucher aux autres.
- Un service exposé en lecture seule protège son état : on ne le modifie que par ses méthodes.
- Dépendre d'une abstraction (
Stockage,HORLOGE) permet de changer d'implémentation… et de tester. - La portée d'un service se choisit : une instance pour l'application (
@Service()), ou une par composant (providers). - Un jeton
multiet une fonctionprovideXxx(withXxx())rendent l'application extensible avec une seule ligne de configuration.
Une bonne architecture ne se voit pas à l'écran : elle se voit le jour où il faut changer le stockage, ajouter une règle ou écrire un test, et que cela ne demande qu'une ligne.