0
Cahier de projet · Atelier

Le projet Atelier.

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.

12 étapes · une vraie app déployée
Vue d'ensemble

Ce que tu construis

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.

Clients

lister, ajouter, chercher — via un formulaire validé et une API.

Projets

rattachés à un client, avec un statut.

Factures

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.

Cours 1 · Séances 1–4

Les fondations

Un projet qui tourne, un premier composant, la navigation SPA, et un build de production.

Étape 1 · Cours 1 · Séance 1

Créer le projet

Objectif

Avoir un projet Angular « Atelier » qui tourne sur ta machine, SSR activé dès le départ.

Étapes
  1. Vérifie Node : node -v (v20+). Sinon installe la version LTS depuis nodejs.org.
  2. Installe le CLI Angular, puis crée le projet.
  3. Lance le serveur de dev et ouvre le navigateur.
Le code (correction)
terminalnpm i -g @angular/cli
ng new atelier --routing --style=scss --ssr
cd atelier
ng serve # → http://localhost:4200
Vérification
  • localhost:4200 affiche la page d'accueil Angular.
  • Tu modifies src/app/app.html, tu sauvegardes → l'écran se met à jour seul.
Étape 2 · Cours 1 · Séance 2

Le premier composant

Objectif

Un modèle Client, un composant ClientCard réutilisable, et une liste de clients affichée avec @for.

Étapes
  1. Crée un type Client dans shared/.
  2. Génère le composant carte : ng g c features/clients/client-card.
  3. Génère la liste : ng g c features/clients/client-list et boucle sur des clients codés en dur.
Le code (correction)
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' },
  ];
}
Vérification
  • En plaçant <app-client-list /> dans app.html, tu vois trois cartes.
  • Changer un nom dans le tableau met à jour la carte correspondante.
Étape 3 · Cours 1 · Séance 3

Navigation & routes

Objectif

Trois écrans — Clients, Projets, Factures — accessibles par une barre de navigation, sans jamais recharger la page.

Étapes
  1. Déclare les routes ; charge une feature en lazy avec loadComponent.
  2. Mets un <router-outlet> et une barre routerLink dans le shell.
  3. Ajoute une route de détail /clients/:id et une route ** (404).
Le code (correction)
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],
Vérification
  • Cliquer dans la barre change l'écran sans flash de rechargement.
  • Dans l'onglet Réseau, la navigation ne redemande pas de page HTML : c'est bien une SPA.
  • Une URL inconnue affiche la page 404.
Étape 4 · Cours 1 · Séance 4

Environnements & build

Objectif

Préparer l'adresse de l'API dans un fichier d'environnement, et produire un build de production.

Étapes
  1. Génère les environnements : ng g environments.
  2. Mets une variable apiUrl dans chaque.
  3. Construis pour la prod et regarde le dossier dist/.
Le code (correction)
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
Vérification
  • dist/atelier/ contient des fichiers JS hachés et un index.html.
  • Le poids total est bien plus petit que le projet source (minification + tree-shaking).
Cours 2 · Séances 1–4

Construire pour de vrai

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.

Étape 5 · Cours 2 · Séance 1

Un service pour les données

Objectif

Sortir les clients des composants et les mettre dans un ClientService partagé, dont l'état vit dans un signal.

Étapes
  1. Génère le service : ng g s core/client.
  2. Mets l'état dans un signal ; expose add().
  3. Dans la liste, remplace le tableau codé en dur par inject(ClientService).
Le code (correction)
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) { ... }
Vérification
  • La liste s'affiche depuis le service.
  • Un bouton qui appelle svc.add(...) fait apparaître le client immédiatement.
Étape 6 · Cours 2 · Séance 2

Brancher l'API (json-server)

Objectif

Remplacer les données en mémoire par de vraies requêtes HTTP vers une fausse API.

Étapes
  1. Installe et lance json-server sur un db.json.
  2. Active provideHttpClient() dans la config.
  3. Le service fait GET et POST ; la liste charge au démarrage.
Le code (correction)
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]));
}
Vérification
  • Au chargement, la liste vient de localhost:3000/clients (visible dans l'onglet Réseau : méthode GET, statut 200).
  • Créer un client fait un POST (statut 201) et l'ajoute dans db.json.
Étape 7 · Cours 2 · Séance 3

Le formulaire « Nouveau client »

Objectif

Un formulaire réactif validé qui crée un client via le service.

Le code (correction)
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();
  }
}
Vérification
  • Le bouton reste désactivé tant que le formulaire est invalide.
  • Un email erroné affiche l'erreur après avoir quitté le champ.
  • Envoyer crée le client (POST) et vide le formulaire.
Étape 8 · Cours 2 · Séance 4

Tableau de bord & recherche

Objectif

Des totaux qui se recalculent tout seuls (computed) et une recherche instantanée.

Le code (correction)
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" /> }
Vérification
  • Le compteur de clients et le CA total changent seuls quand les données changent.
  • Taper dans la recherche filtre la liste en direct, sans bouton.
Cours 3 · Séances 1–4

Niveau expert

La couche pro : sécurité, choix d'architecture, SSR pour le SEO, et la mise en ligne — Atelier livré pour de bon.

Étape 9 · Cours 3 · Séance 1

Sécurité : l'intercepteur

Objectif

Attacher automatiquement un jeton d'authentification à chaque requête sortante.

Le code (correction)
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]))
Vérification
  • Dans l'onglet Réseau, chaque requête porte l'en-tête Authorization: Bearer ….
  • Angular échappe déjà le HTML dans {{ }} — teste en tapant <b>x</b> dans un nom : il s'affiche en texte, pas en gras (protection XSS).
Étape 10 · Cours 3 · Séance 2

OnPush & architecture

Objectif

Accélérer le rendu avec OnPush et ranger le projet proprement.

Le code (correction)
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
Vérification
  • L'app fonctionne toujours avec OnPush (les signals déclenchent le rendu correctement).
  • Chaque feature est lazy-loadée : son JS n'est téléchargé qu'en visitant sa route (onglet Réseau).
Étape 11 · Cours 3 · Séance 3

SSR & SEO

Objectif

Servir du vrai HTML au premier chargement (SSR, activé à l'étape 1) et donner un titre par écran.

Le code (correction)
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.
Vérification
  • « Afficher la source de la page » montre le contenu réel (pas seulement <app-root></app-root>).
  • Le titre de l'onglet change selon l'écran.
Étape 12 · Cours 3 · Séance 4

Déployer Atelier — l'épreuve finale

Objectif

Mettre Atelier en ligne : le front hébergé, pointant vers l'API. C'est le projet fini.

Étapes
  1. Vérifie environment.prod.ts (la vraie apiUrl).
  2. Build de production.
  3. Héberge le contenu de dist/ sur un hébergeur statique ; configure le domaine + HTTPS.
  4. Un pipeline CI reconstruit et redéploie à chaque git push.
Le code (correction)
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
Vérification — le projet est fini si…
  • Atelier est accessible à une vraie URL, en HTTPS.
  • On peut lister, chercher et créer un client en ligne.
  • « Afficher la source » montre du HTML rendu (SSR), le titre change par écran.
  • Un git push redéploie tout seul.