Le guide pas-à-pas du projet fil rouge. Une étape par séance : l'objectif, les étapes, le code corrigé, et comment vérifier. Suis-le du début à la fin — tu repars avec une vraie application Angular qui tourne.
Atelier — un gestionnaire de clients, projets et factures, la version légère de l'outil qu'on utilise en interne chez MarQenti. À la fin : une SPA Angular avec navigation, données réelles via une API, formulaires validés, tableau de bord réactif, sécurité, et mise en ligne.
lister, ajouter, chercher — via un formulaire validé et une API.
rattachés à un client, avec un statut.
montant, payé ou non — et le CA total calculé tout seul.
L'API pendant le cours Angular : on n'a pas encore de backend — on utilise json-server, une fausse API REST qui lit un fichier db.json. Tu apprends les vraies requêtes HTTP sans écrire une ligne de back. Le cours Laravel remplacera json-server par une vraie API — sans rien changer côté Angular.
Comment l'utiliser : après chaque séance, ouvre l'étape correspondante, fais-la, et vérifie avec la case « Vérification ». Ne saute pas d'étape — chacune construit sur la précédente. Le code est la correction : essaie d'abord, compare ensuite.
Un projet qui tourne, un premier composant, la navigation SPA, et un build de production.
Avoir un projet Angular « Atelier » qui tourne sur ta machine, SSR activé dès le départ.
node -v (v20+). Sinon installe la version LTS depuis nodejs.org.terminalnpm i -g @angular/cli ng new atelier --routing --style=scss --ssr cd atelier ng serve # → http://localhost:4200
localhost:4200 affiche la page d'accueil Angular.src/app/app.html, tu sauvegardes → l'écran se met à jour seul.Un modèle Client, un composant ClientCard réutilisable, et une liste de clients affichée avec @for.
Client dans shared/.ng g c features/clients/client-card.ng g c features/clients/client-list et boucle sur des clients codés en dur.src/app/shared/client.model.tsexport interface Client {
id: number;
nom: string;
email: string;
telephone: string;
}
src/app/features/clients/client-card.tsimport { Component, input } from '@angular/core'; import { Client } from '../../shared/client.model'; @Component({ selector: 'app-client-card', standalone: true, template: ` <article class="card"> <h3>{{ client().nom }}</h3> <p>{{ client().email }} · {{ client().telephone }}</p> </article> `, styles: `.card{ border:1px solid #ddd; padding:12px; border-radius:8px; }` }) export class ClientCard { client = input.required<Client>(); // reçu du parent }
src/app/features/clients/client-list.tsimport { Component } from '@angular/core'; import { ClientCard } from './client-card'; import { Client } from '../../shared/client.model'; @Component({ selector: 'app-client-list', standalone: true, imports: [ClientCard], template: ` <h2>Clients</h2> @for (c of clients; track c.id) { <app-client-card [client]="c" /> } ` }) export class ClientList { clients: Client[] = [ { id: 1, nom: 'Boulangerie Zitoun', email: 'z@ex.tn', telephone: '22 000 111' }, { id: 2, nom: 'Garage Sfax Auto', email: 'g@ex.tn', telephone: '22 000 222' }, { id: 3, nom: 'Café des Oliviers', email: 'c@ex.tn', telephone: '22 000 333' }, ]; }
<app-client-list /> dans app.html, tu vois trois cartes.Trois écrans — Clients, Projets, Factures — accessibles par une barre de navigation, sans jamais recharger la page.
loadComponent.<router-outlet> et une barre routerLink dans le shell./clients/:id et une route ** (404).src/app/app.routes.tsimport { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', redirectTo: 'clients', pathMatch: 'full' },
{ path: 'clients', loadComponent: () =>
import('./features/clients/client-list').then(m => m.ClientList) },
{ path: 'clients/:id', loadComponent: () =>
import('./features/clients/client-detail').then(m => m.ClientDetail) },
{ path: 'projets', loadComponent: () =>
import('./features/projets/projet-list').then(m => m.ProjetList) },
{ path: 'factures', loadComponent: () =>
import('./features/factures/facture-list').then(m => m.FactureList) },
{ path: '**', loadComponent: () =>
import('./shared/not-found').then(m => m.NotFound) },
];
src/app/app.html — le shell<nav class="topnav"> <a routerLink="/clients" routerLinkActive="on">Clients</a> <a routerLink="/projets" routerLinkActive="on">Projets</a> <a routerLink="/factures" routerLinkActive="on">Factures</a> </nav> <router-outlet />
src/app/app.ts — importer ce qu'utilise le templateimport { RouterOutlet, RouterLink, RouterLinkActive } from '@angular/router'; // ... imports: [RouterOutlet, RouterLink, RouterLinkActive],
Préparer l'adresse de l'API dans un fichier d'environnement, et produire un build de production.
ng g environments.apiUrl dans chaque.dist/.src/environments/environment.ts (dev)export const environment = { production: false, apiUrl: 'http://localhost:3000', // json-server, étape 6 };
src/environments/environment.prod.tsexport const environment = { production: true, apiUrl: 'https://api.atelier.tn', // la vraie API, plus tard };
terminalng build # → dist/atelier/ (fichiers hachés, minifiés) ls dist/atelier/browser
dist/atelier/ contient des fichiers JS hachés et un index.html.Des données réelles : un service, l'API, un formulaire validé, et un tableau de bord réactif qui se met à jour tout seul.
Sortir les clients des composants et les mettre dans un ClientService partagé, dont l'état vit dans un signal.
ng g s core/client.signal ; expose add().inject(ClientService).src/app/core/client.service.tsimport { Injectable, signal } from '@angular/core'; import { Client } from '../shared/client.model'; @Injectable({ providedIn: 'root' }) // une seule instance partagée export class ClientService { readonly clients = signal<Client[]>([]); add(c: Client) { this.clients.update(list => [...list, c]); } }
client-list.ts — lire depuis le serviceimport { Component, inject } from '@angular/core'; import { ClientService } from '../../core/client.service'; export class ClientList { private svc = inject(ClientService); clients = this.svc.clients; // un signal } // template : @for (c of clients(); track c.id) { ... }
svc.add(...) fait apparaître le client immédiatement.Remplacer les données en mémoire par de vraies requêtes HTTP vers une fausse API.
db.json.provideHttpClient() dans la config.GET et POST ; la liste charge au démarrage.db.json (à la racine) + lancement{ "clients": [ { "id": 1, "nom": "Boulangerie Zitoun", "email": "z@ex.tn", "telephone": "22 000 111" } ], "projets": [], "factures": [] } # terminal (2e onglet) : npx json-server db.json --port 3000
src/app/app.config.tsimport { provideHttpClient } from '@angular/common/http'; export const appConfig = { providers: [ provideRouter(routes), provideHttpClient() ] };
client.service.ts — vraies requêtesimport { HttpClient } from '@angular/common/http'; import { environment } from '../../environments/environment'; private http = inject(HttpClient); private url = `${environment.apiUrl}/clients`; load() { this.http.get<Client[]>(this.url) .subscribe(list => this.clients.set(list)); } create(c: Omit<Client,'id'>) { this.http.post<Client>(this.url, c) // method + url + body .subscribe(saved => this.clients.update(l => [...l, saved])); }
localhost:3000/clients (visible dans l'onglet Réseau : méthode GET, statut 200).POST (statut 201) et l'ajoute dans db.json.Un formulaire réactif validé qui crée un client via le service.
features/clients/client-form.tsimport { Component, inject } from '@angular/core';
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
import { ClientService } from '../../core/client.service';
@Component({
selector: 'app-client-form',
standalone: true,
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="submit()">
<input formControlName="nom" placeholder="Nom">
<input formControlName="email" placeholder="Email">
<input formControlName="telephone" placeholder="Téléphone">
@if (form.controls.email.invalid && form.controls.email.touched) {
<small>Email invalide</small>
}
<button [disabled]="form.invalid">Ajouter</button>
</form>
`
})
export class ClientForm {
private fb = inject(FormBuilder);
private svc = inject(ClientService);
form = this.fb.group({
nom: ['', Validators.required],
email: ['', [Validators.required, Validators.email]],
telephone: ['', Validators.required],
});
submit() {
if (this.form.invalid) return;
this.svc.create(this.form.getRawValue());
this.form.reset();
}
}
Des totaux qui se recalculent tout seuls (computed) et une recherche instantanée.
core/client.service.ts — dérivésreadonly count = computed(() => this.clients().length); readonly query = signal(''); readonly filtered = computed(() => { const q = this.query().toLowerCase(); return this.clients().filter(c => c.nom.toLowerCase().includes(q)); });
factures : CA totalreadonly totalCA = computed(() =>
this.factures().filter(f => f.payee)
.reduce((sum, f) => sum + f.montant, 0));
template — recherche live<input (input)="svc.query.set($any($event.target).value)" placeholder="Chercher…">
@for (c of svc.filtered(); track c.id) { <app-client-card [client]="c" /> }
La couche pro : sécurité, choix d'architecture, SSR pour le SEO, et la mise en ligne — Atelier livré pour de bon.
Attacher automatiquement un jeton d'authentification à chaque requête sortante.
core/auth.interceptor.tsimport { HttpInterceptorFn } from '@angular/common/http'; export const authInterceptor: HttpInterceptorFn = (req, next) => { const token = localStorage.getItem('token'); const authReq = token ? req.clone({ setHeaders: { Authorization: `Bearer ${token}` } }) : req; return next(authReq); };
app.config.ts — le brancherprovideHttpClient(withInterceptors([authInterceptor]))
Authorization: Bearer ….{{ }} — teste en tapant <b>x</b> dans un nom : il s'affiche en texte, pas en gras (protection XSS).Accélérer le rendu avec OnPush et ranger le projet proprement.
sur chaque composantimport { ChangeDetectionStrategy } from '@angular/core'; @Component({ // ... changeDetection: ChangeDetectionStrategy.OnPush, })
l'arborescence ciblesrc/app/ core/ # services, interceptors, guards (une fois) shared/ # modèles, composants réutilisables features/ # clients / projets / factures
Servir du vrai HTML au premier chargement (SSR, activé à l'étape 1) et donner un titre par écran.
dans un composant de pageimport { Title, Meta } from '@angular/platform-browser';
private title = inject(Title);
private meta = inject(Meta);
constructor() {
this.title.setTitle('Clients · Atelier');
this.meta.updateTag({ name: 'description', content: 'Gestion des clients.' });
}
terminal — rendu serveurng build && npm run serve:ssr:atelier # puis « Afficher la source » : le HTML contient déjà le contenu, # pas une page vide remplie par JS.
<app-root></app-root>).Mettre Atelier en ligne : le front hébergé, pointant vers l'API. C'est le projet fini.
environment.prod.ts (la vraie apiUrl).dist/ sur un hébergeur statique ; configure le domaine + HTTPS.git push.terminalng build --configuration production # déploie dist/atelier/browser sur ton hébergeur (Netlify, Vercel, VPS…)
.github/workflows/deploy.yml (esquisse)on: { push: { branches: [main] } } jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci && ng build --configuration production - # étape de déploiement selon l'hébergeur
git push redéploie tout seul.