Développement Web

Angular
Framework complet

Architecture, composants, templates, services, injection de dépendances, RxJS, routing, formulaires réactifs et Signals — Angular 17+ avec standalone components.

Qu'est-ce qu'Angular ?

🅰

Angular est un framework complet (opinionated) développé par Google. Contrairement à React (bibliothèque), Angular fournit tout en standard : routing, formulaires, HTTP, internationalisation, animations, tests. Il utilise TypeScript obligatoirement et repose sur RxJS pour la programmation réactive.

🧩
Components
UI = arbre de composants réutilisables
⚙️
Services
Logique métier injectée via DI
🔗
Modules
Regroupement (ou Standalone depuis v15)
🛣️
Router
Navigation SPA intégrée
📋
Forms
Template-driven & Reactive
🌐
HttpClient
HTTP + intercepteurs intégrés
Angular vs React en un coup d'œil
Angular                    React
─────────────────────────────────────────
Framework complet          Bibliothèque UI
TypeScript obligatoire     JS ou TS
RxJS / Observables         Promesses / hooks
Modules (ou Standalone)    Composants seuls
Template HTML séparé       JSX inline
DI intégrée                Manuelle / contexte
CLI officielle puissante   Vite / CRA / Next
Courbe d'apprentissage     Plus accessible
  plus élevée
Très structuré             Liberté totale
Google                     Meta

Angular brille pour :
→ Applications d'entreprise large échelle
→ Teams multiples avec conventions fortes
→ Backend-for-frontend, portails complexes
→ Projets nécessitant strict typing + DI

Installation & CLI Angular

Créer et démarrer un projet
# Installer Angular CLI globalement
npm install -g @angular/cli

# Créer un nouveau projet
ng new mon-app
# → Routing ? Yes
# → Stylesheet : CSS / SCSS / LESS

cd mon-app
ng serve          # → http://localhost:4200
ng serve --open   # ouvre le navigateur automatiquement

# Build de production
ng build          # → dossier dist/
ng build --watch  # rebuild à chaque modification

# Tests
ng test           # tests unitaires (Karma + Jasmine)
ng e2e            # tests end-to-end

# Structure du projet :
src/
├── app/
│   ├── app.component.ts    ← composant racine
│   ├── app.component.html  ← template HTML
│   ├── app.component.scss
│   ├── app.config.ts       ← config standalone
│   └── app.routes.ts       ← routes
├── assets/
├── index.html
└── main.ts
Générateurs CLI
# Générer un composant
ng generate component pages/home
ng g c pages/home          # version courte
ng g c shared/button --standalone

# Générer un service
ng g service services/auth
ng g s services/user

# Générer un module
ng g module features/admin --routing

# Générer une directive
ng g directive directives/highlight

# Générer un pipe
ng g pipe pipes/currency-fr

# Générer une interface TypeScript
ng g interface models/user

# Générer un guard
ng g guard guards/auth

# Options utiles
--skip-tests      # ne pas créer le fichier .spec.ts
--inline-template # template dans le .ts
--inline-style    # styles dans le .ts
--dry-run         # simuler sans créer les fichiers

Modules & Standalone components

Standalone (Angular 15+, défaut depuis 17)
// Standalone = composant autonome, sans NgModule
// C'est l'approche recommandée depuis Angular 17

// main.ts — démarrage sans AppModule
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig);

// app.config.ts
export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    provideAnimations(),
  ]
};

// Composant standalone
@Component({
  selector: 'app-hero',
  standalone: true,
  imports: [CommonModule, RouterLink],  ← dépendances directes
  template: `<h1>{{titre}}</h1>`
})
export class HeroComponent { ... }
NgModule (approche classique)
// NgModule — regroupe des composants, services, pipes
// Toujours présent dans les anciennes bases de code

@NgModule({
  declarations: [    // composants, directives, pipes du module
    AppComponent,
    NavbarComponent,
    FooterComponent,
  ],
  imports: [         // modules externes utilisés
    BrowserModule,
    HttpClientModule,
    ReactiveFormsModule,
    RouterModule,
  ],
  providers: [       // services disponibles
    AuthService,
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true }
  ],
  exports: [         // exposer aux autres modules
    NavbarComponent,
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Structure recommandée d'un projet

Architecture feature-first
src/app/
├── core/                    ← services singleton, intercepteurs, guards globaux
│   ├── services/
│   │   ├── auth.service.ts
│   │   └── api.service.ts
│   ├── interceptors/
│   │   └── auth.interceptor.ts
│   └── guards/
│       └── auth.guard.ts
│
├── shared/                  ← composants, pipes, directives réutilisables
│   ├── components/
│   │   ├── button/
│   │   ├── modal/
│   │   └── spinner/
│   ├── pipes/
│   │   └── currency-fr.pipe.ts
│   └── directives/
│       └── highlight.directive.ts
│
├── features/                ← fonctionnalités métier (lazy loaded)
│   ├── auth/
│   │   ├── login/           ← login.component.ts + .html + .scss
│   │   ├── register/
│   │   └── auth.routes.ts
│   ├── dashboard/
│   └── users/
│
├── models/                  ← interfaces TypeScript
│   ├── user.model.ts
│   └── product.model.ts
│
├── app.component.ts
├── app.config.ts
└── app.routes.ts

Composants & décorateurs

Anatomie d'un composant Angular
import { Component, Input, Output, EventEmitter, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';

interface User { id: number; nom: string; email: string; }

@Component({
  selector:    'app-user-card',   // <app-user-card>
  standalone:  true,
  imports:     [CommonModule],
  templateUrl: './user-card.component.html',
  styleUrl:    './user-card.component.scss',
})
export class UserCardComponent implements OnInit {

  // Entrée — prop passée depuis le parent
  @Input() user!: User;
  @Input({ required: true }) titre!: string;  // obligatoire

  // Sortie — événement vers le parent
  @Output() deleted = new EventEmitter<number>();

  isExpanded = false;  // propriété de l'état local

  ngOnInit(): void {
    console.log('Composant monté, user :', this.user);
  }

  onDelete(): void {
    this.deleted.emit(this.user.id);
  }
}

// Template parent — utilisation
// <app-user-card [user]="selectedUser" (deleted)="onUserDeleted($event)">
Template HTML associé
<!-- user-card.component.html -->
<div class="card">
  <h2>{{ user.nom }}</h2>        <!-- interpolation -->
  <p>{{ user.email }}</p>

  <!-- Binding de propriété -->
  <img [src]="user.avatar" [alt]="user.nom">

  <!-- Binding d'événement -->
  <button (click)="onDelete()">Supprimer</button>
  <button (click)="isExpanded = !isExpanded">
    {{ isExpanded ? 'Réduire' : 'Voir plus' }}
  </button>

  <!-- Affichage conditionnel -->
  @if (isExpanded) {
    <div class="details">
      <p>ID : {{ user.id }}</p>
    </div>
  }
</div>

<!-- Two-way binding avec ngModel -->
<input [(ngModel)]="searchTerm" placeholder="Rechercher">

<!-- Class et style binding -->
<div [class.active]="isActive">
<div [class]="{ active: isActive, error: hasError }">
<div [style.color]="isError ? 'red' : 'green'">

Templates & directives

Directives structurelles (Angular 17 — syntaxe @)
<!-- @if / @else (Angular 17+ — remplace *ngIf) -->
@if (user) {
  <p>Bonjour {{ user.nom }}</p>
} @else if (loading) {
  <app-spinner />
} @else {
  <p>Veuillez vous connecter</p>
}

<!-- @for (remplace *ngFor) -->
@for (item of items; track item.id) {
  <li>{{ item.nom }}</li>
} @empty {
  <li>Aucun élément</li>
}

<!-- @switch (remplace ngSwitch) -->
@switch (statut) {
  @case ('actif')   { <span class="green">Actif</span> }
  @case ('inactif') { <span class="red">Inactif</span> }
  @default          { <span>Inconnu</span> }
}

<!-- Ancienne syntaxe (toujours supportée) -->
<div *ngIf="user; else loading">...</div>
<ng-template #loading>Chargement...</ng-template>

<li *ngFor="let item of items; trackBy: trackById">
  {{ item.nom }}
</li>
Directives d'attribut personnalisées
// highlight.directive.ts
import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true,
})
export class HighlightDirective {
  @Input() appHighlight = 'yellow';

  constructor(private el: ElementRef) {}

  @HostListener('mouseenter')
  onMouseEnter() {
    this.el.nativeElement.style.backgroundColor = this.appHighlight;
  }

  @HostListener('mouseleave')
  onMouseLeave() {
    this.el.nativeElement.style.backgroundColor = '';
  }
}

<!-- Utilisation -->
<p appHighlight="lightblue">Survolez-moi</p>

// NgClass et NgStyle intégrés
<div [ngClass]="{ 'active': isActive, 'disabled': isDisabled }">
<div [ngStyle]="{ 'font-size': fontSize + 'px', 'color': color }">

Data binding — les 4 types

TypeSyntaxeDirectionUsage
Interpolation{{ value }}TS → HTMLAfficher du texte
Property binding[prop]="expr"TS → HTMLLier une propriété DOM
Event binding(event)="handler()"HTML → TSRéagir aux événements
Two-way binding[(ngModel)]="prop"TS ↔ HTMLFormulaires template-driven
Exemples de bindings
<!-- Interpolation -->
<h1>{{ title }}</h1>
<p>{{ 2 + 2 }}</p>
<p>{{ user?.nom | uppercase }}</p>

<!-- Property binding -->
<input [value]="nom">
<img [src]="imageUrl">
<button [disabled]="!isValid">Envoyer</button>
<app-card [titre]="pageTitle" [user]="currentUser">

<!-- Event binding -->
<button (click)="onSave()">Sauvegarder</button>
<input (input)="onInput($event)">
<form (ngSubmit)="onSubmit()">

<!-- Two-way [()] "banane dans une boîte" -->
<input [(ngModel)]="searchTerm">
<!-- Équivalent à : -->
<input [value]="searchTerm" (input)="searchTerm = $event.target.value">

<!-- Template reference variable -->
<input #emailInput type="email">
<button (click)="doSomething(emailInput.value)">

Cycle de vie des composants

constructor() ngOnChanges() ngOnInit() ngDoCheck() ngAfterContentInit() ngAfterContentChecked() ngAfterViewInit() ngAfterViewChecked() ngOnDestroy()
Hooks essentiels
export class MonComponent implements OnInit, OnDestroy, OnChanges {

  @Input() userId!: number;
  private subscription!: Subscription;

  // Appelé à chaque changement d'@Input()
  ngOnChanges(changes: SimpleChanges): void {
    if (changes['userId']) {
      console.log('userId a changé :', changes['userId'].currentValue);
      this.loadUser(this.userId);
    }
  }

  // Appelé une fois après ngOnChanges initial
  ngOnInit(): void {
    this.loadUser(this.userId);
    this.subscription = this.service.data$.subscribe(d => ...);
  }

  // Appelé après que le DOM est prêt
  ngAfterViewInit(): void {
    // Accéder aux @ViewChild ici (pas dans ngOnInit !)
    this.chart = new Chart(this.canvasRef.nativeElement, ...);
  }

  // TOUJOURS se désabonner pour éviter les memory leaks
  ngOnDestroy(): void {
    this.subscription.unsubscribe();
  }
}
ViewChild & ContentChild
import { ViewChild, ViewChildren, QueryList,
         ElementRef, AfterViewInit } from '@angular/core';

export class ParentComponent implements AfterViewInit {

  // Référencer un élément DOM
  @ViewChild('myInput') inputRef!: ElementRef;

  // Référencer un composant enfant
  @ViewChild(ChartComponent) chart!: ChartComponent;

  // Référencer plusieurs enfants
  @ViewChildren(ItemComponent) items!: QueryList<ItemComponent>;

  ngAfterViewInit() {
    this.inputRef.nativeElement.focus();
    this.chart.render();
    this.items.forEach(item => item.init());
  }
}

<!-- Template -->
<input #myInput type="text">
<app-chart />

// ContentChild — projeté via ng-content
@ContentChild(TitleComponent) titleComp!: TitleComponent;

Pipes

Pipes intégrés
<!-- Texte -->
{{ 'bonjour' | uppercase }}      <!-- BONJOUR -->
{{ 'BONJOUR' | lowercase }}      <!-- bonjour -->
{{ 'jean dupont' | titlecase }}  <!-- Jean Dupont -->
{{ texte | slice:0:50 }}         <!-- 50 premiers caractères -->

<!-- Nombres -->
{{ 3.14159 | number:'1.2-2' }}   <!-- 3,14 -->
{{ 0.85 | percent }}             <!-- 85% -->
{{ 1234.5 | currency:'EUR':'symbol':'1.2-2' }}  <!-- 1 234,50 € -->

<!-- Dates -->
{{ today | date }}                  <!-- 06/03/2026 -->
{{ today | date:'dd/MM/yyyy' }}     <!-- 06/03/2026 -->
{{ today | date:'EEEE d MMMM y' }}  <!-- vendredi 6 mars 2026 -->
{{ today | date:'shortTime' }}      <!-- 14:30 -->

<!-- Objets -->
{{ objet | json }}               <!-- débogage -->
{{ observable$ | async }}        <!-- subscribe automatique -->
{{ value | keyvalue }}           <!-- itérer sur objet avec @for -->
Pipe personnalisé
// ng g pipe pipes/truncate
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'truncate',
  standalone: true,
  pure: true,  // default — recalcule seulement si input change
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit = 50, trail = '...'): string {
    if (!value) return '';
    return value.length > limit
      ? value.substring(0, limit) + trail
      : value;
  }
}

<!-- Utilisation -->
{{ description | truncate:100:'…' }}

// Pipe avec async — gérer les Observables
// ✅ Évite de s'abonner / désabonner manuellement
export class UserListComponent {
  users$ = this.userService.getAll();  // Observable<User[]>
}

<!-- Template -->
@if (users$ | async; as users) {
  @for (user of users; track user.id) {
    <app-user-card [user]="user" />
  }
}

Services & injection de dépendances

Créer et injecter un service
// ng g service services/auth
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'  // singleton accessible partout
})
export class AuthService {
  private http = inject(HttpClient);   // inject() function (Angular 14+)

  private userSubject = new BehaviorSubject<User | null>(null);
  readonly user$ = this.userSubject.asObservable();
  readonly isLoggedIn$ = this.user$.pipe(map(u => !!u));

  login(email: string, password: string): Observable<User> {
    return this.http.post<User>('/api/auth/login', { email, password })
      .pipe(tap(user => this.userSubject.next(user)));
  }

  logout(): void {
    this.userSubject.next(null);
  }
}

// Injection dans un composant
export class NavbarComponent {
  // Méthode 1 — inject() function (Angular 14+, recommandé)
  private authService = inject(AuthService);

  // Méthode 2 — constructeur (classique)
  constructor(private authService: AuthService) {}
}
Scopes de l'injection de dépendances
// providedIn: 'root'   → 1 instance pour toute l'app
// providedIn: 'any'    → 1 instance par lazy-loaded module
// Dans un composant    → instance détruite avec le composant

// Fournir dans un composant — scope local
@Component({
  selector: 'app-wizard',
  providers: [WizardService],  // ← instance propre à ce composant
})
export class WizardComponent { ... }

// Token d'injection — valeurs et config
import { InjectionToken, inject } from '@angular/core';

export const API_URL = new InjectionToken<string>('API_URL');

// Dans app.config.ts
providers: [
  { provide: API_URL, useValue: 'https://api.example.com' }
]

// Dans un service
private apiUrl = inject(API_URL);

// Intercepteur HTTP
export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const token = inject(AuthService).getToken();
  const cloned = req.clone({ headers: req.headers.set('Authorization', `Bearer ${token}`) });
  return next(cloned);
};

HttpClient

Service HTTP complet
import { Injectable, inject } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { Observable, catchError, map, throwError } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class UserService {
  private http = inject(HttpClient);
  private apiUrl = '/api/users';

  // GET liste
  getAll(page = 1, limit = 10): Observable<User[]> {
    const params = new HttpParams()
      .set('page', page).set('limit', limit);
    return this.http.get<User[]>(this.apiUrl, { params });
  }

  // GET par id
  getById(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }

  // POST
  create(user: Partial<User>): Observable<User> {
    return this.http.post<User>(this.apiUrl, user);
  }

  // PUT / PATCH
  update(id: number, user: Partial<User>): Observable<User> {
    return this.http.patch<User>(`${this.apiUrl}/${id}`, user);
  }

  // DELETE
  delete(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }
}
Utiliser dans un composant
export class UserListComponent implements OnInit, OnDestroy {
  private userService = inject(UserService);
  users: User[] = [];
  loading = true;
  error = '';
  private sub!: Subscription;

  ngOnInit() {
    this.sub = this.userService.getAll()
      .pipe(
        catchError(err => {
          this.error = err.message;
          return [];
        })
      )
      .subscribe({
        next:     users => { this.users = users; this.loading = false; },
        error:    err   => { this.error = err.message; this.loading = false; },
        complete: ()    => { this.loading = false; },
      });
  }

  ngOnDestroy() { this.sub.unsubscribe(); }
}

// Alternative moderne — async pipe (pas besoin de unsubscribe)
export class UserListComponent {
  private userService = inject(UserService);
  users$ = this.userService.getAll();
}
<!-- template : -->
@if (users$ | async; as users) {
  @for (u of users; track u.id) { <app-user-card [user]="u" /> }
}

Observables & RxJS

Opérateurs RxJS essentiels
import { map, filter, switchMap, debounceTime,
         catchError, tap, of, combineLatest,
         takeUntilDestroyed } from 'rxjs';

// map — transformer chaque valeur
users$.pipe(map(users => users.filter(u => u.actif)));

// filter — garder si condition
events$.pipe(filter(e => e.type === 'click'));

// switchMap — annuler la précédente requête
// Parfait pour la recherche auto
searchTerm$.pipe(
  debounceTime(300),
  switchMap(term => this.http.get(`/search?q=${term}`))
);

// catchError — gérer les erreurs
users$.pipe(
  catchError(err => {
    console.error(err);
    return of([]);  // retourner tableau vide
  })
);

// tap — effet de bord sans modifier la valeur
users$.pipe(tap(users => console.log('reçu', users.length)));

// combineLatest — combiner plusieurs streams
combineLatest([user$, permissions$]).pipe(
  map(([user, perms]) => ({ ...user, perms }))
);
Subjects & BehaviorSubject
import { Subject, BehaviorSubject, ReplaySubject } from 'rxjs';

// Subject — observable + observer manuel
const événement$ = new Subject<string>();
événement$.next('click');     // émettre
événement$.subscribe(e => ...); // s'abonner

// BehaviorSubject — a une valeur initiale + mémorise la dernière
private cartSubject = new BehaviorSubject<Item[]>([]);
cart$ = this.cartSubject.asObservable();   // exposer en lecture seule
this.cartSubject.next([...items, newItem]); // mettre à jour
this.cartSubject.getValue();               // valeur synchrone

// ReplaySubject — mémorise N dernières valeurs
const replay$ = new ReplaySubject<number>(3);
// Un nouvel abonné reçoit immédiatement les 3 dernières valeurs

// takeUntilDestroyed (Angular 16+) — désabonnement automatique
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';

export class MyComponent {
  private destroyRef = inject(DestroyRef);

  ngOnInit() {
    this.data$.pipe(
      takeUntilDestroyed(this.destroyRef)  // ← auto-unsubscribe !
    ).subscribe(d => this.data = d);
  }
}

Routing

Configuration des routes
// app.routes.ts
export const routes: Routes = [
  { path: '',         redirectTo: '/home', pathMatch: 'full' },
  { path: 'home',     component: HomeComponent },
  { path: 'about',    component: AboutComponent },

  // Paramètre de route
  { path: 'users/:id', component: UserDetailComponent },

  // Lazy loading — ne charge le module qu'à la demande
  {
    path: 'admin',
    loadChildren: () => import('./features/admin/admin.routes')
                          .then(m => m.adminRoutes),
    canActivate: [authGuard],
  },

  // Route avec données statiques
  { path: 'aide', component: HelpComponent,
    data: { title: 'Centre d\'aide' } },

  // Wildcard
  { path: '**', component: NotFoundComponent },
];

<!-- router-outlet dans app.component.html -->
<router-outlet />
Navigation & paramètres
import { RouterLink, RouterLinkActive,
         ActivatedRoute, Router } from '@angular/router';

<!-- Template -->
<a routerLink="/home" routerLinkActive="active">Accueil</a>
<a [routerLink]="['/users', userId]">Mon profil</a>
<a [routerLink]="['/search']" [queryParams]="{ q: term }">

// Navigation programmée
export class LoginComponent {
  private router = inject(Router);

  onLogin() {
    this.router.navigate(['/dashboard']);
    this.router.navigate(['/users', userId]);
    this.router.navigateByUrl('/home');
  }
}

// Lire les paramètres
export class UserDetailComponent {
  private route = inject(ActivatedRoute);

  userId = this.route.snapshot.params['id'];      // synchrone
  userId$ = this.route.paramMap.pipe(              // reactif
    map(params => +params.get('id')!));

  queryStr = this.route.snapshot.queryParams['q']; // ?q=value
}

Guards & resolvers

Functional guards (Angular 14+)
// auth.guard.ts
import { inject } from '@angular/core';
import { CanActivateFn, Router } from '@angular/router';

export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router      = inject(Router);

  if (authService.isLoggedIn()) {
    return true;
  }
  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url }
  });
};

// Guard pour vérifier un rôle
export const adminGuard: CanActivateFn = () => {
  const authService = inject(AuthService);
  return authService.hasRole('admin');
};

// Types de guards :
// canActivate    — accéder à une route ?
// canDeactivate  — quitter une route ? (ex: formulaire non sauvegardé)
// canLoad        — charger un lazy module ?
// canMatch       — matcher une route ?
// resolve        — charger des données avant la navigation
Resolver — précharger des données
// user.resolver.ts — charge l'utilisateur avant le render
export const userResolver: ResolveFn<User> = (route) => {
  const userService = inject(UserService);
  const id = +route.paramMap.get('id')!;
  return userService.getById(id);
};

// Dans les routes :
{
  path: 'users/:id',
  component: UserDetailComponent,
  resolve: { user: userResolver }
}

// Dans le composant :
export class UserDetailComponent {
  private route = inject(ActivatedRoute);

  user = this.route.snapshot.data['user'] as User;
  // Données disponibles IMMÉDIATEMENT, pas besoin de loader
}

Reactive Forms

FormBuilder & validateurs
import { FormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';

@Component({ imports: [ReactiveFormsModule, ...] })
export class RegisterComponent {
  private fb = inject(FormBuilder);

  form = this.fb.group({
    nom: ['', [Validators.required, Validators.minLength(2)]],
    email: ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, Validators.minLength(8)]],
    confirm: ['', Validators.required],
    age: [null, [Validators.min(18), Validators.max(120)]],
  }, { validators: passwordMatchValidator });

  get nomCtrl()   { return this.form.get('nom')!; }
  get emailCtrl() { return this.form.get('email')!; }

  onSubmit() {
    if (this.form.invalid) return;
    console.log(this.form.value);
  }
}

// Validateur personnalisé
const passwordMatchValidator = (group: AbstractControl) => {
  const pass    = group.get('password')?.value;
  const confirm = group.get('confirm')?.value;
  return pass === confirm ? null : { passwordMismatch: true };
};
Template du formulaire réactif
<form [formGroup]="form" (ngSubmit)="onSubmit()">

  <label>Nom
    <input formControlName="nom" />
    @if (nomCtrl.invalid && nomCtrl.touched) {
      @if (nomCtrl.hasError('required'))   { <span>Requis</span> }
      @if (nomCtrl.hasError('minlength')) {
        <span>Minimum {{ nomCtrl.errors?.['minlength'].requiredLength }} car.</span>
      }
    }
  </label>

  <label>Email
    <input formControlName="email" type="email" />
    @if (emailCtrl.invalid && emailCtrl.dirty) {
      <span>Email invalide</span>
    }
  </label>

  <button type="submit" [disabled]="form.invalid">
    S'inscrire
  </button>

</form>

<!-- Accès programmatique -->
form.get('email')?.value         // lire une valeur
form.patchValue({ nom: 'Alice' }) // mettre à jour partiellement
form.setValue({ nom, email, ... }) // mettre à jour tout
form.reset()                      // remettre à zéro

Signals — Angular 16+

signal, computed, effect
import { signal, computed, effect } from '@angular/core';

export class CartComponent {
  // signal — état réactif (remplace BehaviorSubject + ngZone)
  items = signal<Item[]>([]);
  count = signal(0);

  // computed — valeur dérivée (recalcule si dépendances changent)
  total = computed(() =>
    this.items().reduce((sum, i) => sum + i.prix, 0)
  );
  hasItems = computed(() => this.items().length > 0);

  // effect — effet de bord (ex: sync localStorage)
  constructor() {
    effect(() => {
      localStorage.setItem('cart', JSON.stringify(this.items()));
      // Relancé automatiquement quand items() change
    });
  }

  addItem(item: Item) {
    this.items.update(current => [...current, item]);
    this.count.update(n => n + 1);
  }

  removeItem(id: number) {
    this.items.update(items => items.filter(i => i.id !== id));
    this.count.set(this.items().length);  // set = remplacer
  }
}

<!-- Template — lire un signal avec () -->
<p>{{ count() }} articles — Total : {{ total() | currency }}</p>
toSignal / toObservable — interopérabilité
import { toSignal, toObservable } from '@angular/core/rxjs-interop';

export class SearchComponent {
  private userService = inject(UserService);

  // Convertir un Observable en Signal
  users = toSignal(
    this.userService.getAll(),
    { initialValue: [] }
  );

  // Signal pour le terme de recherche
  searchTerm = signal('');

  // Convertir signal en Observable pour utiliser switchMap
  results = toSignal(
    toObservable(this.searchTerm).pipe(
      debounceTime(300),
      switchMap(term => this.userService.search(term))
    ),
    { initialValue: [] }
  );
}

// Template — plus besoin du pipe async !
@for (user of users(); track user.id) {
  <app-user-card [user]="user" />
}

Performance & ChangeDetection

OnPush — détection de changements optimisée
// Par défaut, Angular vérifie TOUT l'arbre de composants
// à chaque événement (clic, timer, requête HTTP).

// OnPush — vérifier seulement si :
// 1. Une @Input() a changé de référence
// 2. Un événement vient de CE composant
// 3. Un Observable avec async pipe émet
// 4. markForCheck() est appelé manuellement

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush,
  // ...
})
export class UserCardComponent {
  @Input() user!: User;
  // Recalculé SEULEMENT si la référence de user change
  // ✅ Ne muter JAMAIS l'objet — créer un nouvel objet
}

// Forcer la détection si nécessaire
private cdr = inject(ChangeDetectorRef);
this.cdr.markForCheck();   // marquer pour la prochaine passe
this.cdr.detectChanges();  // forcer immédiatement

// Avec les Signals → OnPush automatiquement optimal
// les signals notifient directement le composant
Lazy loading & optimisations
// Lazy loading de routes (déjà vu) — crucial pour perf
{
  path: 'admin',
  loadComponent: () => import('./admin.component')
                         .then(c => c.AdminComponent),
}

// @defer — rendu différé (Angular 17+)
<!-- Rend le composant seulement quand visible -->
@defer (on viewport) {
  <app-heavy-chart />
} @loading {
  <app-spinner />
} @placeholder {
  <div>Graphique...</div>
}

// @defer conditions :
// on idle           → quand le navigateur est inactif
// on viewport       → quand visible dans le viewport
// on interaction    → quand l'utilisateur interagit
// on timer(2000)    → après 2 secondes
// when condition    → quand condition est vraie

// trackBy dans @for — éviter de re-créer les DOM nodes
@for (item of items; track item.id) {
  <app-item [item]="item" />
}

Cheat sheet Angular

CLI essentiels

ng newCréer un projet
ng serveDev server port 4200
ng g cGénérer un composant
ng g sGénérer un service
ng buildBuild production
ng testTests unitaires

Template syntax

{{ val }}Interpolation
[prop]="expr"Property binding
(event)="fn()"Event binding
[(ngModel)]Two-way binding
@if / @forDirectives structurelles
| asyncS'abonner à un Observable

Décorateurs clés

@ComponentDéclarer un composant
@InjectableDéclarer un service
@Input()Recevoir une prop parent
@Output()Émettre vers le parent
@ViewChildRéférencer un enfant DOM

Signals (Angular 16+)

signal(val)État réactif
.set(val)Remplacer la valeur
.update(fn)Modifier via fonction
computed(fn)Valeur dérivée
effect(fn)Effet de bord réactif