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.
01 — Architecture
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
02
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
03
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 { }
04
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
05 — Composants
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'">
06
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 }">
07
Data binding — les 4 types
| Type | Syntaxe | Direction | Usage |
|---|---|---|---|
| Interpolation | {{ value }} | TS → HTML | Afficher du texte |
| Property binding | [prop]="expr" | TS → HTML | Lier une propriété DOM |
| Event binding | (event)="handler()" | HTML → TS | Réagir aux événements |
| Two-way binding | [(ngModel)]="prop" | TS ↔ HTML | Formulaires 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)">
08
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;
09
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" />
}
}
10 — Services & DI
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);
};
11
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" /> }
}
12
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);
}
}
13 — Routing & Forms
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
}
14
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
}
15
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
16 — Avancé
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" />
}
17
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" />
}
18 — Référence
Cheat sheet Angular
CLI essentiels
| ng new | Créer un projet |
| ng serve | Dev server port 4200 |
| ng g c | Générer un composant |
| ng g s | Générer un service |
| ng build | Build production |
| ng test | Tests unitaires |
Template syntax
| {{ val }} | Interpolation |
| [prop]="expr" | Property binding |
| (event)="fn()" | Event binding |
| [(ngModel)] | Two-way binding |
| @if / @for | Directives structurelles |
| | async | S'abonner à un Observable |
Décorateurs clés
| @Component | Déclarer un composant |
| @Injectable | Déclarer un service |
| @Input() | Recevoir une prop parent |
| @Output() | Émettre vers le parent |
| @ViewChild | Ré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 |