# Fichier: python_cheats/cheatsheets/angular.txt
# Cheatsheet Angular - Guide Complet pour Débutants


[OK] INTRODUCTION À ANGULAR

Angular est un framework JavaScript/TypeScript développé par Google pour créer des applications web single-page (SPA).
Il fournit une structure complète avec routing, formulaires, HTTP client, tests, et plus.

# Caractéristiques principales:
- TypeScript par défaut (typage statique)
- Architecture basée sur les composants
- Two-way data binding
- Dependency Injection
- RxJS pour la programmation réactive
- CLI puissant pour le développement
- Écosystème riche et mature

# Versions:
- AngularJS (1.x) - Ancien, obsolète
- Angular (2+) - Moderne, réécriture complète
- Version actuelle: Angular 17+ (Standalone Components par défaut)


[OK] INSTALLATION & CONFIGURATION

# Prérequis
# Node.js 18.13+ ou 20.9+ requis
node --version
npm --version

# Installer Node.js
# https://nodejs.org/
# Ou avec nvm (Node Version Manager)
nvm install 20
nvm use 20

# Installer Angular CLI globalement
npm install -g @angular/cli

# Vérifier installation
ng version
ng v

# Mettre à jour Angular CLI
npm update -g @angular/cli

# Mettre à jour CLI locale dans projet
ng update @angular/cli

# Désinstaller Angular CLI
npm uninstall -g @angular/cli


[OK] CRÉER UN NOUVEAU PROJET

# Créer nouveau projet (mode interactif)
ng new mon-projet

# Questions posées:
# - Routing? Yes/No
# - Stylesheet format? CSS/SCSS/SASS/LESS
# - SSR (Server-Side Rendering)? Yes/No

# Créer avec options en ligne de commande
ng new mon-projet --routing --style=scss
ng new mon-projet --routing=false --style=css
ng new mon-projet --skip-git --skip-install
ng new mon-projet --package-manager=yarn
ng new mon-projet --standalone=true           # Standalone (défaut Angular 17+)
ng new mon-projet --standalone=false          # NgModule (ancien style)

# Options complètes
ng new mon-projet \
  --routing=true \
  --style=scss \
  --skip-tests=false \
  --package-manager=npm \
  --strict=true \
  --prefix=app

# Structure créée:
mon-projet/
├── .angular/                # Cache build
├── .vscode/                 # Configuration VSCode
├── node_modules/            # Dépendances npm
├── src/
│   ├── app/                 # Code application
│   │   ├── app.component.ts
│   │   ├── app.component.html
│   │   ├── app.component.scss
│   │   ├── app.component.spec.ts
│   │   ├── app.config.ts    # Configuration (standalone)
│   │   └── app.routes.ts    # Routes (standalone)
│   ├── assets/              # Images, fonts, etc.
│   ├── environments/        # Configurations environnements
│   ├── index.html           # Page HTML principale
│   ├── main.ts              # Point d'entrée
│   └── styles.scss          # Styles globaux
├── .editorconfig
├── .gitignore
├── angular.json             # Configuration Angular CLI
├── package.json             # Dépendances npm
├── tsconfig.json            # Configuration TypeScript
├── tsconfig.app.json
└── tsconfig.spec.json


[OK] COMMANDES DE BASE

# Naviguer dans le projet
cd mon-projet

# Lancer serveur de développement
ng serve
ng serve --open                      # Ouvre navigateur automatiquement
ng serve -o                          # Raccourci pour --open
ng serve --port 4300                 # Port personnalisé (défaut: 4200)
ng serve --host 0.0.0.0              # Accessible depuis réseau
ng serve --ssl                       # HTTPS
ng serve --proxy-config proxy.conf.json

# URLs par défaut:
# http://localhost:4200
# Hot reload activé automatiquement

# Build pour production
ng build
ng build --configuration production  # Production optimisée
ng build --prod                      # Raccourci (déprécié Angular 12+)
ng build --base-href /mon-app/       # Base URL personnalisée
ng build --output-path=dist/custom   # Dossier de sortie
ng build --watch                     # Rebuild automatique

# Fichiers générés dans dist/
dist/mon-projet/
├── browser/                 # Application compilée
│   ├── index.html
│   ├── main-[hash].js
│   ├── polyfills-[hash].js
│   └── styles-[hash].css
└── server/                  # SSR (si activé)

# Lancer tests unitaires
ng test
ng test --watch=false        # Exécution unique
ng test --code-coverage      # Avec couverture de code
ng test --browsers=Chrome
ng test --include='**/*.spec.ts'

# Lancer tests end-to-end
ng e2e

# Linting (avec ESLint)
ng lint
ng lint --fix                # Correction automatique

# Générer documentation
npm install -g @compodoc/compodoc
compodoc -p tsconfig.json -s

# Analyser bundle
npm install -g webpack-bundle-analyzer
ng build --stats-json
webpack-bundle-analyzer dist/mon-projet/stats.json

# Mettre à jour Angular
ng update                    # Voir mises à jour disponibles
ng update @angular/core @angular/cli
ng update --all --force


[OK] COMPOSANTS (COMPONENTS)

Les composants sont les blocs de base d'Angular. Chaque composant contrôle une partie de l'écran.

# Générer un composant
ng generate component mon-composant
ng g c mon-composant                             # Raccourci
ng g c mon-composant --skip-tests                # Sans fichier test
ng g c mon-composant --inline-template           # Template inline
ng g c mon-composant --inline-style              # Style inline
ng g c mon-composant --flat                      # Sans dossier
ng g c mon-composant --standalone=true           # Standalone (défaut)
ng g c mon-composant --standalone=false          # NgModule

# Générer dans sous-dossier
ng g c features/mon-composant
ng g c shared/components/mon-composant

# Structure générée:
src/app/mon-composant/
├── mon-composant.component.ts        # Logique
├── mon-composant.component.html      # Template
├── mon-composant.component.scss      # Styles
└── mon-composant.component.spec.ts   # Tests

# === COMPOSANT STANDALONE (Angular 17+) ===

// mon-composant.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';

@Component({
  selector: 'app-mon-composant',
  standalone: true,
  imports: [CommonModule],              // Importer autres composants/modules
  templateUrl: './mon-composant.component.html',
  styleUrls: ['./mon-composant.component.scss']
})
export class MonComposantComponent {
  // Propriétés
  titre: string = 'Mon Composant';
  compteur: number = 0;
  utilisateurs: string[] = ['Alice', 'Bob', 'Charlie'];
  estVisible: boolean = true;
  
  // Méthodes
  incrementer(): void {
    this.compteur++;
  }
  
  afficherMessage(): void {
    console.log('Bouton cliqué!');
  }
  
  ajouterUtilisateur(nom: string): void {
    this.utilisateurs.push(nom);
  }
}

# === TEMPLATE (HTML) ===

<!-- mon-composant.component.html -->

<!-- Interpolation -->
<h1>{{ titre }}</h1>
<p>Compteur: {{ compteur }}</p>
<p>Total utilisateurs: {{ utilisateurs.length }}</p>

<!-- Property Binding -->
<img [src]="imageUrl" [alt]="imageAlt">
<button [disabled]="compteur >= 10">Incrémenter</button>
<div [class.active]="estActif">Contenu</div>
<div [style.color]="couleur">Texte coloré</div>

<!-- Event Binding -->
<button (click)="incrementer()">Cliquer</button>
<input (input)="onInput($event)" (keyup.enter)="valider()">
<div (mouseenter)="onMouseEnter()" (mouseleave)="onMouseLeave()">
  Survolez-moi
</div>

<!-- Two-way Binding -->
<input [(ngModel)]="nom" placeholder="Votre nom">
<p>Bonjour {{ nom }}!</p>

<!-- Structural Directives -->

<!-- *ngIf -->
<div *ngIf="estVisible">Contenu visible</div>
<div *ngIf="compteur > 5; else autreContenu">
  Compteur supérieur à 5
</div>
<ng-template #autreContenu>
  <p>Compteur inférieur ou égal à 5</p>
</ng-template>

<!-- *ngFor -->
<ul>
  <li *ngFor="let utilisateur of utilisateurs">
    {{ utilisateur }}
  </li>
</ul>

<!-- *ngFor avec index et autres variables -->
<div *ngFor="let item of items; let i = index; let first = first; let last = last">
  {{ i }}: {{ item }} 
  <span *ngIf="first">(premier)</span>
  <span *ngIf="last">(dernier)</span>
</div>

<!-- *ngSwitch -->
<div [ngSwitch]="couleurPreferee">
  <p *ngSwitchCase="'rouge'">Vous aimez le rouge</p>
  <p *ngSwitchCase="'bleu'">Vous aimez le bleu</p>
  <p *ngSwitchCase="'vert'">Vous aimez le vert</p>
  <p *ngSwitchDefault>Couleur inconnue</p>
</div>

<!-- Attribute Directives -->

<!-- ngClass -->
<div [ngClass]="{'active': estActif, 'disabled': estDesactive}">
  Contenu
</div>
<div [ngClass]="classesDynamiques">Contenu</div>

<!-- ngStyle -->
<div [ngStyle]="{'color': couleur, 'font-size': taille + 'px'}">
  Texte stylé
</div>
<div [ngStyle]="stylesDynamiques">Contenu</div>

<!-- Template Reference Variables -->
<input #monInput type="text">
<button (click)="traiter(monInput.value)">Valider</button>

<!-- Pipes -->
<p>{{ date | date:'dd/MM/yyyy' }}</p>
<p>{{ prix | currency:'EUR' }}</p>
<p>{{ texte | uppercase }}</p>
<p>{{ texte | lowercase }}</p>
<p>{{ nombre | number:'1.2-2' }}</p>
<p>{{ donnees | json }}</p>
<p>{{ tableau | slice:0:5 }}</p>

# === STYLES (SCSS) ===

// mon-composant.component.scss

// Styles encapsulés au composant
:host {
  display: block;
  padding: 20px;
}

.titre {
  color: #333;
  font-size: 24px;
}

.bouton {
  background: #007bff;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  
  &:hover {
    background: #0056b3;
  }
  
  &:disabled {
    background: #ccc;
    cursor: not-allowed;
  }
}

# === TEMPLATE ET STYLES INLINE ===

@Component({
  selector: 'app-mon-composant',
  standalone: true,
  template: `
    <h1>{{ titre }}</h1>
    <button (click)="incrementer()">{{ compteur }}</button>
  `,
  styles: [`
    :host {
      display: block;
    }
    h1 {
      color: blue;
    }
  `]
})
export class MonComposantComponent {
  titre = 'Mon Composant';
  compteur = 0;
  
  incrementer() {
    this.compteur++;
  }
}

# === ENCAPSULATION DES STYLES ===

import { ViewEncapsulation } from '@angular/core';

@Component({
  selector: 'app-mon-composant',
  templateUrl: './mon-composant.component.html',
  styleUrls: ['./mon-composant.component.scss'],
  encapsulation: ViewEncapsulation.Emulated  // Défaut: styles encapsulés
  // ViewEncapsulation.None                  // Styles globaux
  // ViewEncapsulation.ShadowDom            // Shadow DOM natif
})


[OK] DATA BINDING

# === TYPES DE BINDING ===

# 1. Interpolation {{ }}
<h1>{{ titre }}</h1>
<p>{{ 2 + 2 }}</p>
<p>{{ getNom() }}</p>

# 2. Property Binding []
<img [src]="imageUrl">
<button [disabled]="estDesactive">Cliquer</button>
<div [innerHTML]="htmlContent"></div>

# 3. Event Binding ()
<button (click)="onClick()">Cliquer</button>
<input (input)="onInput($event)">
<form (submit)="onSubmit($event)">

# 4. Two-way Binding [()]
<input [(ngModel)]="nom">

# Équivalent à:
<input [ngModel]="nom" (ngModelChange)="nom = $event">

# === EXEMPLES PRATIQUES ===

// Composant
export class ExempleComponent {
  nom: string = '';
  age: number = 0;
  email: string = '';
  accepteConditions: boolean = false;
  couleurPreferee: string = 'bleu';
  
  onSubmit(): void {
    console.log({
      nom: this.nom,
      age: this.age,
      email: this.email,
      accepteConditions: this.accepteConditions,
      couleurPreferee: this.couleurPreferee
    });
  }
}

<!-- Template -->
<form (submit)="onSubmit()">
  <!-- Input text -->
  <input type="text" [(ngModel)]="nom" name="nom">
  
  <!-- Input number -->
  <input type="number" [(ngModel)]="age" name="age">
  
  <!-- Input email -->
  <input type="email" [(ngModel)]="email" name="email">
  
  <!-- Checkbox -->
  <input type="checkbox" [(ngModel)]="accepteConditions" name="conditions">
  
  <!-- Select -->
  <select [(ngModel)]="couleurPreferee" name="couleur">
    <option value="rouge">Rouge</option>
    <option value="bleu">Bleu</option>
    <option value="vert">Vert</option>
  </select>
  
  <button type="submit">Envoyer</button>
</form>

<!-- Affichage des valeurs -->
<p>Nom: {{ nom }}</p>
<p>Age: {{ age }}</p>
<p>Email: {{ email }}</p>
<p>Conditions: {{ accepteConditions }}</p>
<p>Couleur: {{ couleurPreferee }}</p>


[OK] COMMUNICATION ENTRE COMPOSANTS

# === @INPUT - Parent vers Enfant ===

// Composant enfant
import { Component, Input } from '@angular/core';

@Component({
  selector: 'app-enfant',
  standalone: true,
  template: `
    <h2>{{ titre }}</h2>
    <p>{{ message }}</p>
  `
})
export class EnfantComponent {
  @Input() titre: string = '';
  @Input() message: string = '';
  @Input() utilisateur?: { nom: string; age: number };
  
  // Input avec alias
  @Input('nomPersonnalise') nom: string = '';
  
  // Input avec transformation
  @Input()
  set compteur(value: number) {
    this._compteur = value * 2;
  }
  get compteur(): number {
    return this._compteur;
  }
  private _compteur: number = 0;
}

// Composant parent
import { EnfantComponent } from './enfant.component';

@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [EnfantComponent],
  template: `
    <app-enfant 
      [titre]="monTitre"
      [message]="monMessage"
      [utilisateur]="user">
    </app-enfant>
  `
})
export class ParentComponent {
  monTitre = 'Titre depuis Parent';
  monMessage = 'Message depuis Parent';
  user = { nom: 'Alice', age: 30 };
}

# === @OUTPUT - Enfant vers Parent ===

// Composant enfant
import { Component, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-enfant',
  standalone: true,
  template: `
    <button (click)="envoyer()">Envoyer</button>
    <input #input type="text">
    <button (click)="envoyerTexte(input.value)">Envoyer Texte</button>
  `
})
export class EnfantComponent {
  @Output() messageEnvoye = new EventEmitter<string>();
  @Output() donneeEnvoyee = new EventEmitter<any>();
  
  envoyer(): void {
    this.messageEnvoye.emit('Hello depuis enfant!');
  }
  
  envoyerTexte(texte: string): void {
    this.donneeEnvoyee.emit({ texte, timestamp: Date.now() });
  }
}

// Composant parent
@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [EnfantComponent],
  template: `
    <app-enfant 
      (messageEnvoye)="recevoirMessage($event)"
      (donneeEnvoyee)="recevoirDonnee($event)">
    </app-enfant>
    <p>Message reçu: {{ messageRecu }}</p>
  `
})
export class ParentComponent {
  messageRecu: string = '';
  
  recevoirMessage(message: string): void {
    this.messageRecu = message;
    console.log('Message reçu:', message);
  }
  
  recevoirDonnee(donnee: any): void {
    console.log('Donnée reçue:', donnee);
  }
}

# === TEMPLATE REFERENCE & ViewChild ===

// Accéder aux éléments et composants enfants

import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';

@Component({
  selector: 'app-parent',
  standalone: true,
  imports: [EnfantComponent],
  template: `
    <input #monInput type="text">
    <button (click)="focusInput()">Focus Input</button>
    
    <app-enfant #enfant></app-enfant>
    <button (click)="appellerEnfant()">Appeler Enfant</button>
  `
})
export class ParentComponent implements AfterViewInit {
  @ViewChild('monInput') inputRef!: ElementRef;
  @ViewChild('enfant') enfantComponent!: EnfantComponent;
  
  ngAfterViewInit(): void {
    // Accès disponible après initialisation de la vue
    console.log(this.inputRef.nativeElement.value);
  }
  
  focusInput(): void {
    this.inputRef.nativeElement.focus();
  }
  
  appellerEnfant(): void {
    this.enfantComponent.uneMethode();
  }
}

# === SERVICE PARTAGÉ (Recommandé) ===

// data.service.ts
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class DataService {
  private messageSource = new BehaviorSubject<string>('Message initial');
  message$ = this.messageSource.asObservable();
  
  changerMessage(message: string): void {
    this.messageSource.next(message);
  }
}

// Composant 1
@Component({
  selector: 'app-composant1',
  standalone: true,
  template: `
    <input [(ngModel)]="nouveauMessage">
    <button (click)="envoyer()">Envoyer</button>
  `
})
export class Composant1Component {
  nouveauMessage = '';
  
  constructor(private dataService: DataService) {}
  
  envoyer(): void {
    this.dataService.changerMessage(this.nouveauMessage);
  }
}

// Composant 2
@Component({
  selector: 'app-composant2',
  standalone: true,
  template: `<p>{{ message }}</p>`
})
export class Composant2Component {
  message = '';
  
  constructor(private dataService: DataService) {
    this.dataService.message$.subscribe(msg => {
      this.message = msg;
    });
  }
}


[OK] SERVICES & DEPENDENCY INJECTION

Les services sont des classes réutilisables pour partager données et logique.

# Générer un service
ng generate service services/mon-service
ng g s services/mon-service
ng g s services/mon-service --skip-tests

# Structure générée:
src/app/services/
├── mon-service.service.ts
└── mon-service.service.spec.ts

# === SERVICE DE BASE ===

// mon-service.service.ts
import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root'  // Service singleton disponible partout
})
export class MonServiceService {
  private donnees: any[] = [];
  
  constructor() {
    console.log('Service initialisé');
  }
  
  getDonnees(): any[] {
    return this.donnees;
  }
  
  ajouterDonnee(donnee: any): void {
    this.donnees.push(donnee);
  }
  
  supprimerDonnee(index: number): void {
    this.donnees.splice(index, 1);
  }
  
  viderDonnees(): void {
    this.donnees = [];
  }
}

# === UTILISER UN SERVICE ===

// Injecter dans composant
import { MonServiceService } from './services/mon-service.service';

@Component({
  selector: 'app-mon-composant',
  standalone: true,
  templateUrl: './mon-composant.component.html'
})
export class MonComposantComponent {
  donnees: any[] = [];
  
  // Injection dans constructeur
  constructor(private monService: MonServiceService) {
    this.donnees = this.monService.getDonnees();
  }
  
  ajouter(donnee: any): void {
    this.monService.ajouterDonnee(donnee);
    this.donnees = this.monService.getDonnees();
  }
}

# === NIVEAUX DE PROVISION ===

# 1. Root (Singleton - Recommandé)
@Injectable({
  providedIn: 'root'
})
// Une seule instance pour toute l'application

# 2. Au niveau du composant
@Component({
  selector: 'app-mon-composant',
  standalone: true,
  providers: [MonService]  // Nouvelle instance pour ce composant
})
// Nouvelle instance pour chaque instance du composant

# 3. Au niveau d'un module (ancien style)
@NgModule({
  providers: [MonService]
})
// Une instance partagée dans le module

# === SERVICE HTTP ===

// user.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface User {
  id: number;
  name: string;
  email: string;
}

@Injectable({
  providedIn: 'root'
})
export class UserService {
  private apiUrl = 'https://api.example.com/users';
  
  constructor(private http: HttpClient) {}
  
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.apiUrl);
  }
  
  getUser(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/${id}`);
  }
  
  createUser(user: User): Observable<User> {
    return this.http.post<User>(this.apiUrl, user);
  }
  
  updateUser(id: number, user: User): Observable<User> {
    return this.http.put<User>(`${this.apiUrl}/${id}`, user);
  }
  
  deleteUser(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`);
  }
}

# Utiliser dans composant
@Component({
  selector: 'app-users',
  standalone: true
})
export class UsersComponent implements OnInit {
  users: User[] = [];
  
  constructor(private userService: UserService) {}
  
  ngOnInit(): void {
    this.userService.getUsers().subscribe({
      next: (data) => {
        this.users = data;
      },
      error: (error) => {
        console.error('Erreur:', error);
      },
      complete: () => {
        console.log('Requête terminée');
      }
    });
  }
}

# === SERVICE AVEC RXJS ===

import { BehaviorSubject, Subject, Observable } from 'rxjs';
import { map, filter, debounceTime } from 'rxjs/operators';

@Injectable({
  providedIn: 'root'
})
export class StateService {
  // BehaviorSubject: garde la dernière valeur
  private userSubject = new BehaviorSubject<User | null>(null);
  user$ = this.userSubject.asObservable();
  
  // Subject: pas de valeur initiale
  private notificationSubject = new Subject<string>();
  notification$ = this.notificationSubject.asObservable();
  
  setUser(user: User): void {
    this.userSubject.next(user);
  }
  
  clearUser(): void {
    this.userSubject.next(null);
  }
  
  notifier(message: string): void {
    this.notificationSubject.next(message);
  }
  
  // Observable avec transformation
  getUserName$(): Observable<string> {
    return this.user$.pipe(
      filter(user => user !== null),
      map(user => user!.name)
    );
  }
}


[OK] ROUTING & NAVIGATION

Le routing permet de naviguer entre différentes vues (composants).

# === CONFIGURATION DE BASE (Standalone) ===

// app.routes.ts
import { Routes } from '@angular/router';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';
import { ContactComponent } from './contact/contact.component';

export const routes: Routes = [
  { path: '', component: HomeComponent },           // Route par défaut
  { path: 'about', component: AboutComponent },
  { path: 'contact', component: ContactComponent },
  { path: '**', redirectTo: '' }                    // Route 404
];

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes)
  ]
};

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig)
  .catch(err => console.error(err));

# === LAZY LOADING STANDALONE ===

// app.routes.ts
export const routes: Routes = [
  {
    path: 'users',
    loadComponent: () => import('./users/users.component')
      .then(m => m.UsersComponent)
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes')
      .then(m => m.ADMIN_ROUTES)
  }
];

// admin/admin.routes.ts
import { Routes } from '@angular/router';
import { AdminComponent } from './admin.component';
import { DashboardComponent } from './dashboard/dashboard.component';

export const ADMIN_ROUTES: Routes = [
  {
    path: '',
    component: AdminComponent,
    children: [
      { path: 'dashboard', component: DashboardComponent }
    ]
  }
];

# === MIGRER DE NGMODULE À STANDALONE ===

# Commande de migration automatique
ng generate @angular/core:standalone

# Options
ng generate @angular/core:standalone --mode=convert-to-standalone
ng generate @angular/core:standalone --mode=prune-ng-modules
ng generate @angular/core:standalone --mode=standalone-bootstrap


[OK] FORMULAIRES

Angular propose deux approches: Template-driven et Reactive Forms.

# === TEMPLATE-DRIVEN FORMS ===

// Importer FormsModule
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-form-template',
  standalone: true,
  imports: [FormsModule],
  templateUrl: './form-template.component.html'
})
export class FormTemplateComponent {
  utilisateur = {
    nom: '',
    email: '',
    age: null,
    accepte: false
  };
  
  onSubmit(): void {
    console.log('Formulaire soumis:', this.utilisateur);
  }
}

<!-- Template -->
<form #monForm="ngForm" (ngSubmit)="onSubmit()">
  <!-- Input text -->
  <div>
    <label>Nom:</label>
    <input 
      type="text" 
      name="nom"
      [(ngModel)]="utilisateur.nom"
      required
      minlength="3"
      #nom="ngModel">
    
    <!-- Messages d'erreur -->
    <div *ngIf="nom.invalid && (nom.dirty || nom.touched)">
      <span *ngIf="nom.errors?.['required']">Le nom est requis</span>
      <span *ngIf="nom.errors?.['minlength']">
        Minimum 3 caractères ({{ nom.errors?.['minlength'].actualLength }}/3)
      </span>
    </div>
  </div>
  
  <!-- Input email -->
  <div>
    <label>Email:</label>
    <input 
      type="email" 
      name="email"
      [(ngModel)]="utilisateur.email"
      required
      email
      #email="ngModel">
    
    <div *ngIf="email.invalid && email.touched">
      <span *ngIf="email.errors?.['required']">Email requis</span>
      <span *ngIf="email.errors?.['email']">Email invalide</span>
    </div>
  </div>
  
  <!-- Input number -->
  <div>
    <label>Âge:</label>
    <input 
      type="number" 
      name="age"
      [(ngModel)]="utilisateur.age"
      min="18"
      max="100"
      #age="ngModel">
  </div>
  
  <!-- Checkbox -->
  <div>
    <label>
      <input 
        type="checkbox" 
        name="accepte"
        [(ngModel)]="utilisateur.accepte"
        required>
      J'accepte les conditions
    </label>
  </div>
  
  <button type="submit" [disabled]="monForm.invalid">
    Envoyer
  </button>
  
  <!-- État du formulaire -->
  <pre>{{ monForm.value | json }}</pre>
  <p>Valide: {{ monForm.valid }}</p>
  <p>Pristine: {{ monForm.pristine }}</p>
  <p>Touched: {{ monForm.touched }}</p>
</form>

# === REACTIVE FORMS (Recommandé) ===

// Importer ReactiveFormsModule
import { Component, OnInit } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { FormBuilder, FormGroup, FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app-form-reactive',
  standalone: true,
  imports: [ReactiveFormsModule],
  templateUrl: './form-reactive.component.html'
})
export class FormReactiveComponent implements OnInit {
  formulaire!: FormGroup;
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit(): void {
    // Méthode 1: FormBuilder (recommandé)
    this.formulaire = this.fb.group({
      nom: ['', [Validators.required, Validators.minLength(3)]],
      email: ['', [Validators.required, Validators.email]],
      age: [null, [Validators.min(18), Validators.max(100)]],
      telephone: ['', [Validators.pattern(/^[0-9]{10}$/)]],
      adresse: this.fb.group({
        rue: [''],
        ville: ['', Validators.required],
        codePostal: ['', [Validators.required, Validators.pattern(/^[0-9]{5}$/)]]
      }),
      accepte: [false, Validators.requiredTrue]
    });
    
    // Méthode 2: Sans FormBuilder
    this.formulaire = new FormGroup({
      nom: new FormControl('', [Validators.required]),
      email: new FormControl('', [Validators.required, Validators.email])
    });
  }
  
  onSubmit(): void {
    if (this.formulaire.valid) {
      console.log('Formulaire valide:', this.formulaire.value);
    } else {
      console.log('Formulaire invalide');
      this.formulaire.markAllAsTouched();
    }
  }
  
  // Getters pour accès facile dans template
  get nom() {
    return this.formulaire.get('nom');
  }
  
  get email() {
    return this.formulaire.get('email');
  }
  
  get ville() {
    return this.formulaire.get('adresse.ville');
  }
}

<!-- Template -->
<form [formGroup]="formulaire" (ngSubmit)="onSubmit()">
  <!-- Input simple -->
  <div>
    <label>Nom:</label>
    <input type="text" formControlName="nom">
    
    <div *ngIf="nom?.invalid && (nom?.dirty || nom?.touched)">
      <span *ngIf="nom?.errors?.['required']">Nom requis</span>
      <span *ngIf="nom?.errors?.['minlength']">Minimum 3 caractères</span>
    </div>
  </div>
  
  <!-- Input email -->
  <div>
    <label>Email:</label>
    <input type="email" formControlName="email">
    
    <div *ngIf="email?.invalid && email?.touched">
      <span *ngIf="email?.errors?.['required']">Email requis</span>
      <span *ngIf="email?.errors?.['email']">Email invalide</span>
    </div>
  </div>
  
  <!-- FormGroup imbriqué -->
  <div formGroupName="adresse">
    <div>
      <label>Rue:</label>
      <input type="text" formControlName="rue">
    </div>
    
    <div>
      <label>Ville:</label>
      <input type="text" formControlName="ville">
      <div *ngIf="ville?.invalid && ville?.touched">
        <span *ngIf="ville?.errors?.['required']">Ville requise</span>
      </div>
    </div>
    
    <div>
      <label>Code Postal:</label>
      <input type="text" formControlName="codePostal">
    </div>
  </div>
  
  <!-- Checkbox -->
  <div>
    <label>
      <input type="checkbox" formControlName="accepte">
      J'accepte les conditions
    </label>
  </div>
  
  <button type="submit" [disabled]="formulaire.invalid">
    Envoyer
  </button>
  
  <!-- Debug -->
  <pre>{{ formulaire.value | json }}</pre>
  <p>Valide: {{ formulaire.valid }}</p>
</form>

# === FORMARRAY (Champs dynamiques) ===

import { FormArray } from '@angular/forms';

@Component({
  selector: 'app-form-array',
  standalone: true,
  imports: [ReactiveFormsModule, CommonModule]
})
export class FormArrayComponent implements OnInit {
  formulaire!: FormGroup;
  
  constructor(private fb: FormBuilder) {}
  
  ngOnInit(): void {
    this.formulaire = this.fb.group({
      nom: [''],
      hobbies: this.fb.array([])
    });
  }
  
  get hobbies(): FormArray {
    return this.formulaire.get('hobbies') as FormArray;
  }
  
  ajouterHobby(): void {
    this.hobbies.push(this.fb.control('', Validators.required));
  }
  
  supprimerHobby(index: number): void {
    this.hobbies.removeAt(index);
  }
  
  onSubmit(): void {
    console.log(this.formulaire.value);
  }
}

<!-- Template -->
<form [formGroup]="formulaire" (ngSubmit)="onSubmit()">
  <div>
    <label>Nom:</label>
    <input type="text" formControlName="nom">
  </div>
  
  <div>
    <h3>Hobbies</h3>
    <div formArrayName="hobbies">
      <div *ngFor="let hobby of hobbies.controls; let i = index">
        <input [formControlName]="i" placeholder="Hobby {{ i + 1 }}">
        <button type="button" (click)="supprimerHobby(i)">Supprimer</button>
      </div>
    </div>
    <button type="button" (click)="ajouterHobby()">Ajouter Hobby</button>
  </div>
  
  <button type="submit">Envoyer</button>
</form>

# === VALIDATEURS PERSONNALISÉS ===

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';

// Validateur fonction
export function ageMinimumValidator(min: number): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    if (!control.value) {
      return null;
    }
    return control.value >= min ? null : { ageMinimum: { min, actual: control.value } };
  };
}

// Validateur async (vérifier en base de données)
export function emailUniqueValidator(userService: UserService): ValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    if (!control.value) {
      return of(null);
    }
    return userService.verifierEmail(control.value).pipe(
      map(existe => existe ? { emailExiste: true } : null)
    );
  };
}

// Utilisation
this.formulaire = this.fb.group({
  age: [null, [ageMinimumValidator(18)]],
  email: ['', [Validators.required], [emailUniqueValidator(this.userService)]]
});

# === RÉAGIR AUX CHANGEMENTS ===

ngOnInit(): void {
  this.formulaire = this.fb.group({
    prenom: [''],
    nom: [''],
    email: ['']
  });
  
  // Écouter changements d'un champ
  this.formulaire.get('prenom')?.valueChanges.subscribe(value => {
    console.log('Prénom changé:', value);
  });
  
  // Écouter changements du formulaire
  this.formulaire.valueChanges.subscribe(value => {
    console.log('Formulaire changé:', value);
  });
  
  // Avec debounce
  this.formulaire.get('email')?.valueChanges.pipe(
    debounceTime(500),
    distinctUntilChanged()
  ).subscribe(value => {
    console.log('Email après 500ms:', value);
  });
  
  // Écouter statut
  this.formulaire.statusChanges.subscribe(status => {
    console.log('Status:', status); // VALID, INVALID, PENDING
  });
}

# === MANIPULATION PROGRAMMATIQUE ===

// Définir valeur
this.formulaire.patchValue({
  nom: 'Dupont',
  email: 'dupont@example.com'
});

// Définir valeur complète (toutes les propriétés requises)
this.formulaire.setValue({
  nom: 'Dupont',
  email: 'dupont@example.com',
  age: 30,
  telephone: '0123456789',
  adresse: {
    rue: '123 Rue',
    ville: 'Paris',
    codePostal: '75001'
  },
  accepte: true
});

// Réinitialiser
this.formulaire.reset();
this.formulaire.reset({ nom: 'Valeur par défaut' });

// Désactiver/Activer
this.formulaire.get('nom')?.disable();
this.formulaire.get('nom')?.enable();
this.formulaire.disable();
this.formulaire.enable();

// Marquer comme touché
this.formulaire.markAsTouched();
this.formulaire.markAllAsTouched();

// Obtenir valeur brute (inclut champs désactivés)
const valeurComplete = this.formulaire.getRawValue();


[OK] HTTP CLIENT

Pour communiquer avec des APIs REST.

# === CONFIGURATION ===

// app.config.ts (Standalone)
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient()
  ]
};

// Avec intercepteurs
import { provideHttpClient, withInterceptors } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([authInterceptor, loggingInterceptor])
    )
  ]
};

# === SERVICE HTTP ===

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http';
import { Observable, throwError } from 'rxjs';
import { catchError, retry, map } from 'rxjs/operators';

export interface User {
  id: number;
  name: string;
  email: string;
}

@Injectable({
  providedIn: 'root'
})
export class ApiService {
  private apiUrl = 'https://api.example.com';
  
  constructor(private http: HttpClient) {}
  
  // === GET ===
  
  // GET simple
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(`${this.apiUrl}/users`);
  }
  
  // GET avec paramètres
  getUser(id: number): Observable<User> {
    return this.http.get<User>(`${this.apiUrl}/users/${id}`);
  }
  
  // GET avec query parameters
  rechercherUsers(query: string, page: number = 1): Observable<User[]> {
    const params = new HttpParams()
      .set('q', query)
      .set('page', page.toString())
      .set('limit', '10');
    
    return this.http.get<User[]>(`${this.apiUrl}/users/search`, { params });
  }
  
  // GET avec headers personnalisés
  getUsersAvecAuth(): Observable<User[]> {
    const headers = new HttpHeaders({
      'Authorization': 'Bearer token123',
      'Content-Type': 'application/json'
    });
    
    return this.http.get<User[]>(`${this.apiUrl}/users`, { headers });
  }
  
  // === POST ===
  
  createUser(user: Omit<User, 'id'>): Observable<User> {
    return this.http.post<User>(`${this.apiUrl}/users`, user);
  }
  
  // POST avec headers
  createUserAvecHeaders(user: Omit<User, 'id'>): Observable<User> {
    const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
    return this.http.post<User>(`${this.apiUrl}/users`, user, { headers });
  }
  
  // === PUT ===
  
  updateUser(id: number, user: User): Observable<User> {
    return this.http.put<User>(`${this.apiUrl}/users/${id}`, user);
  }
  
  // === PATCH ===
  
  updateUserPartiel(id: number, data: Partial<User>): Observable<User> {
    return this.http.patch<User>(`${this.apiUrl}/users/${id}`, data);
  }
  
  // === DELETE ===
  
  deleteUser(id: number): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/users/${id}`);
  }
  
  // === GESTION D'ERREURS ===
  
  getUsersAvecGestionErreurs(): Observable<User[]> {
    return this.http.get<User[]>(`${this.apiUrl}/users`).pipe(
      retry(3),  // Réessayer 3 fois
      catchError(this.handleError)
    );
  }
  
  private handleError(error: any): Observable<never> {
    console.error('Erreur API:', error);
    
    if (error.status === 0) {
      console.error('Erreur réseau');
    } else {
      console.error(`Code: ${error.status}, Message: ${error.message}`);
    }
    
    return throwError(() => new Error('Une erreur est survenue'));
  }
  
  // === TRANSFORMATION DE DONNÉES ===
  
  getUsersTransformes(): Observable<string[]> {
    return this.http.get<User[]>(`${this.apiUrl}/users`).pipe(
      map(users => users.map(u => u.name))
    );
  }
  
  // === REQUÊTE COMPLÈTE ===
  
  getUserComplet(id: number): Observable<any> {
    return this.http.get(`${this.apiUrl}/users/${id}`, {
      observe: 'response'  // Obtenir réponse complète (headers, status, body)
    }).pipe(
      map(response => {
        console.log('Headers:', response.headers.keys());
        console.log('Status:', response.status);
        return response.body;
      })
    );
  }
  
  // === UPLOAD DE FICHIER ===
  
  uploadFile(file: File): Observable<any> {
    const formData = new FormData();
    formData.append('file', file);
    
    return this.http.post(`${this.apiUrl}/upload`, formData, {
      reportProgress: true,
      observe: 'events'
    });
  }
  
  // === DOWNLOAD DE FICHIER ===
  
  downloadFile(id: number): Observable<Blob> {
    return this.http.get(`${this.apiUrl}/files/${id}`, {
      responseType: 'blob'
    });
  }
}

# === UTILISATION DANS COMPOSANT ===

@Component({
  selector: 'app-users',
  standalone: true,
  template: `
    <div *ngIf="loading">Chargement...</div>
    <div *ngIf="error">{{ error }}</div>
    
    <ul *ngIf="users.length > 0">
      <li *ngFor="let user of users">
        {{ user.name }} - {{ user.email }}
        <button (click)="deleteUser(user.id)">Supprimer</button>
      </li>
    </ul>
    
    <button (click)="loadUsers()">Recharger</button>
  `
})
export class UsersComponent implements OnInit, OnDestroy {
  users: User[] = [];
  loading = false;
  error: string | null = null;
  private destroy$ = new Subject<void>();
  
  constructor(private apiService: ApiService) {}
  
  ngOnInit(): void {
    this.loadUsers();
  }
  
  loadUsers(): void {
    this.loading = true;
    this.error = null;
    
    this.apiService.getUsers().pipe(
      takeUntil(this.destroy$)
    ).subscribe({
      next: (data) => {
        this.users = data;
        this.loading = false;
      },
      error: (err) => {
        this.error = 'Erreur lors du chargement';
        this.loading = false;
        console.error(err);
      },
      complete: () => {
        console.log('Requête terminée');
      }
    });
  }
  
  deleteUser(id: number): void {
    if (confirm('Confirmer la suppression?')) {
      this.apiService.deleteUser(id).subscribe({
        next: () => {
          this.users = this.users.filter(u => u.id !== id);
        },
        error: (err) => console.error(err)
      });
    }
  }
  
  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

# === INTERCEPTEURS ===

// Ajouter automatiquement un header à toutes les requêtes

import { HttpInterceptorFn } from '@angular/common/http';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  // Récupérer token
  const token = localStorage.getItem('auth_token');
  
  if (token) {
    // Cloner requête et ajouter header
    const authReq = req.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
    return next(authReq);
  }
  
  return next(req);
};

// Intercepteur de logging
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
  console.log('Requête:', req.method, req.url);
  const started = Date.now();
  
  return next(req).pipe(
    tap(event => {
      if (event.type === HttpEventType.Response) {
        const elapsed = Date.now() - started;
        console.log(`Réponse en ${elapsed}ms:`, event.status);
      }
    })
  );
};

// Intercepteur d'erreurs
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
  return next(req).pipe(
    catchError((error: HttpErrorResponse) => {
      if (error.status === 401) {
        // Rediriger vers login
        console.error('Non autorisé');
      } else if (error.status === 500) {
        console.error('Erreur serveur');
      }
      return throwError(() => error);
    })
  );
};


[OK] RXJS & OBSERVABLES

Angular utilise extensivement RxJS pour la programmation réactive.

# === CONCEPTS DE BASE ===

import { Observable, of, from, interval, Subject, BehaviorSubject } from 'rxjs';
import { map, filter, tap, catchError, switchMap, mergeMap, debounceTime } from 'rxjs/operators';

// Créer Observable
const observable$ = new Observable(subscriber => {
  subscriber.next(1);
  subscriber.next(2);
  subscriber.next(3);
  subscriber.complete();
});

// Souscrire
observable$.subscribe({
  next: (value) => console.log(value),
  error: (err) => console.error(err),
  complete: () => console.log('Terminé')
});

// Créer depuis valeurs
const of$ = of(1, 2, 3);
const from$ = from([1, 2, 3]);
const fromPromise$ = from(fetch('https://api.example.com/data'));

// Observable interval
const interval$ = interval(1000); // Émet toutes les secondes

# === SUBJECTS ===

// Subject: Multicast (plusieurs subscribers)
const subject = new Subject<number>();

subject.subscribe(val => console.log('A:', val));
subject.subscribe(val => console.log('B:', val));

subject.next(1);  // A: 1, B: 1
subject.next(2);  // A: 2, B: 2

// BehaviorSubject: Garde la dernière valeur
const behaviorSubject = new BehaviorSubject<number>(0);

behaviorSubject.subscribe(val => console.log('A:', val));  // A: 0 (immédiat)
behaviorSubject.next(1);  // A: 1
behaviorSubject.subscribe(val => console.log('B:', val));  // B: 1 (dernière valeur)
behaviorSubject.next(2);  // A: 2, B: 2

// ReplaySubject: Rejoue N dernières valeurs
const replaySubject = new ReplaySubject<number>(2);

replaySubject.next(1);
replaySubject.next(2);
replaySubject.next(3);
replaySubject.subscribe(val => console.log(val));  // 2, 3

# === OPÉRATEURS PRINCIPAUX ===

import { Component, OnInit } from '@angular/core';

@Component({
  selector: 'app-rxjs-demo',
  standalone: true
})
export class RxjsDemoComponent implements OnInit {
  
  ngOnInit(): void {
    // MAP: Transformer les valeurs
    of(1, 2, 3).pipe(
      map(x => x * 2)
    ).subscribe(val => console.log(val));  // 2, 4, 6
    
    // FILTER: Filtrer les valeurs
    of(1, 2, 3, 4, 5).pipe(
      filter(x => x % 2 === 0)
    ).subscribe(val => console.log(val));  // 2, 4
    
    // TAP: Action secondaire (debugging)
    of(1, 2, 3).pipe(
      tap(x => console.log('Avant:', x)),
      map(x => x * 2),
      tap(x => console.log('Après:', x))
    ).subscribe();
    
    // DEBOUNCE: Attendre un délai
    // Utile pour search input
    fromEvent(input, 'input').pipe(
      debounceTime(500),
      map((event: any) => event.target.value)
    ).subscribe(value => this.rechercher(value));
    
    // DISTINCT UNTIL CHANGED: Ignorer valeurs identiques
    of(1, 1, 2, 2, 3, 1).pipe(
      distinctUntilChanged()
    ).subscribe(val => console.log(val));  // 1, 2, 3, 1
    
    // TAKE: Prendre N valeurs
    interval(1000).pipe(
      take(5)
    ).subscribe(val => console.log(val));  // 0, 1, 2, 3, 4 puis complete
    
    // SKIP: Ignorer N valeurs
    of(1, 2, 3, 4, 5).pipe(
      skip(2)
    ).subscribe(val => console.log(val));  // 3, 4, 5
    
    // FIRST / LAST
    of(1, 2, 3).pipe(first()).subscribe(val => console.log(val));  // 1
    of(1, 2, 3).pipe(last()).subscribe(val => console.log(val));   // 3
  }
  
  rechercher(query: string): void {
    console.log('Recherche:', query);
  }
}

# === OPÉRATEURS DE COMBINAISON ===

// MERGE: Combine plusieurs observables
import { merge } from 'rxjs';

const obs1$ = of(1, 2, 3);
const obs2$ = of(4, 5, 6);

merge(obs1$, obs2$).subscribe(val => console.log(val));
// 1, 2, 3, 4, 5, 6

// CONCAT: Combine séquentiellement
import { concat } from 'rxjs';

concat(obs1$, obs2$).subscribe(val => console.log(val));
// Attend que obs1$ termine avant obs2$

// COMBINELATEST: Combine dernières valeurs
import { combineLatest } from 'rxjs';

const temp$ = of(20, 25, 30);
const humidity$ = of(50, 60);

combineLatest([temp$, humidity$]).subscribe(([temp, hum]) => {
  console.log(`Temp: ${temp}°C, Humidité: ${hum}%`);
});

// FORKJOIN: Attend que tous terminent (comme Promise.all)
import { forkJoin } from 'rxjs';

forkJoin({
  users: this.http.get('/api/users'),
  posts: this.http.get('/api/posts'),
  comments: this.http.get('/api/comments')
}).subscribe(({ users, posts, comments }) => {
  console.log('Tout chargé!', users, posts, comments);
});

# === OPÉRATEURS DE TRANSFORMATION ===

// SWITCHMAP: Annule la requête précédente
// Parfait pour autocomplete/search
searchControl.valueChanges.pipe(
  debounceTime(300),
  switchMap(query => this.apiService.search(query))
).subscribe(results => this.results = results);

// MERGEMAP (flatMap): Toutes les requêtes en parallèle
userIds$.pipe(
  mergeMap(id => this.apiService.getUser(id))
).subscribe(user => console.log(user));

// CONCATMAP: Requêtes séquentielles (attend chaque)
userIds$.pipe(
  concatMap(id => this.apiService.getUser(id))
).subscribe(user => console.log(user));

// EXHAUSTMAP: Ignore nouvelles requêtes jusqu'à fin
saveButton.clicks$.pipe(
  exhaustMap(() => this.apiService.save(data))
).subscribe();

# === GESTION D'ERREURS ===

// CATCHERROR: Capturer erreurs
this.http.get('/api/users').pipe(
  catchError(error => {
    console.error('Erreur:', error);
    return of([]);  // Retourner valeur par défaut
  })
).subscribe(users => console.log(users));

// RETRY: Réessayer N fois
this.http.get('/api/users').pipe(
  retry(3),
  catchError(error => {
    console.error('Échec après 3 tentatives');
    return throwError(() => error);
  })
).subscribe();

// RETRYWHEN: Réessayer avec logique personnalisée
this.http.get('/api/users').pipe(
  retryWhen(errors => 
    errors.pipe(
      delay(1000),
      take(3)
    )
  )
).subscribe();

# === UNSUBSCRIBE (IMPORTANT!) ===

import { Subject, takeUntil } from 'rxjs';

@Component({
  selector: 'app-exemple',
  standalone: true
})
export class ExempleComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();
  
  ngOnInit(): void {
    // Méthode 1: takeUntil (Recommandé)
    this.service.getData().pipe(
      takeUntil(this.destroy$)
    ).subscribe(data => {
      console.log(data);
    });
    
    // Plusieurs souscriptions
    this.service.users$.pipe(
      takeUntil(this.destroy$)
    ).subscribe();
    
    this.service.posts$.pipe(
      takeUntil(this.destroy$)
    ).subscribe();
  }
  
  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

// Méthode 2: Subscription manuelle
subscription!: Subscription;

ngOnInit(): void {
  this.subscription = this.service.getData().subscribe();
}

ngOnDestroy(): void {
  this.subscription.unsubscribe();
}

// Méthode 3: Async pipe (automatique)
// Template
users$ = this.service.getUsers();

<div *ngFor="let user of users$ | async">
  {{ user.name }}
</div>

# === EXEMPLE PRATIQUE: RECHERCHE TEMPS RÉEL ===

@Component({
  selector: 'app-search',
  standalone: true,
  imports: [ReactiveFormsModule, CommonModule],
  template: `
    <input [formControl]="searchControl" placeholder="Rechercher...">
    <div *ngIf="loading">Chargement...</div>
    <ul>
      <li *ngFor="let result of results">{{ result.name }}</li>
    </ul>
  `
})
export class SearchComponent implements OnInit, OnDestroy {
  searchControl = new FormControl('');
  results: any[] = [];
  loading = false;
  private destroy$ = new Subject<void>();
  
  constructor(private apiService: ApiService) {}
  
  ngOnInit(): void {
    this.searchControl.valueChanges.pipe(
      debounceTime(300),          // Attendre 300ms après frappe
      distinctUntilChanged(),     // Ignorer si valeur identique
      tap(() => this.loading = true),
      switchMap(query =>          // Annuler requête précédente
        query 
          ? this.apiService.search(query)
          : of([])
      ),
      tap(() => this.loading = false),
      takeUntil(this.destroy$)
    ).subscribe(results => {
      this.results = results;
    });
  }
  
  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
  }
}


[OK] PIPES

Les pipes transforment les données dans les templates.

# === PIPES INTÉGRÉS ===

<!-- DatePipe -->
{{ dateActuelle | date }}                    <!-- Nov 22, 2025 -->
{{ dateActuelle | date:'short' }}            <!-- 11/22/25, 3:30 PM -->
{{ dateActuelle | date:'medium' }}           <!-- Nov 22, 2025, 3:30:00 PM -->
{{ dateActuelle | date:'long' }}             <!-- November 22, 2025 at 3:30:00 PM GMT+1 -->
{{ dateActuelle | date:'full' }}             <!-- Saturday, November 22, 2025 at 3:30:00 PM -->
{{ dateActuelle | date:'dd/MM/yyyy' }}       <!-- 22/11/2025 -->
{{ dateActuelle | date:'HH:mm:ss' }}         <!-- 15:30:00 -->

<!-- CurrencyPipe -->
{{ prix | currency }}                        <!-- $100.00 -->
{{ prix | currency:'EUR' }}                  <!-- €100.00 -->
{{ prix | currency:'EUR':'symbol':'1.2-2':'fr' }}  <!-- 100,00 € -->

<!-- DecimalPipe -->
{{ nombre | number }}                        <!-- 1,234.567 -->
{{ nombre | number:'1.0-0' }}                <!-- 1,235 (arrondi) -->
{{ nombre | number:'1.2-2' }}                <!-- 1,234.57 -->
{{ nombre | number:'3.1-5' }}                <!-- 001,234.567 -->

<!-- PercentPipe -->
{{ 0.25 | percent }}                         <!-- 25% -->
{{ 0.25 | percent:'1.2-2' }}                 <!-- 25.00% -->

<!-- UpperCasePipe / LowerCasePipe -->
{{ 'angular' | uppercase }}                  <!-- ANGULAR -->
{{ 'ANGULAR' | lowercase }}                  <!-- angular -->

<!-- TitleCasePipe -->
{{ 'angular framework' | titlecase }}        <!-- Angular Framework -->

<!-- SlicePipe -->
{{ [1,2,3,4,5] | slice:1:4 }}               <!-- [2,3,4] -->
{{ 'Angular' | slice:0:3 }}                  <!-- Ang -->

<!-- JsonPipe (Debug) -->
<pre>{{ objet | json }}</pre>                <!-- Format JSON indenté -->

<!-- AsyncPipe (Observables) -->
{{ users$ | async }}
<div *ngFor="let user of users$ | async">
  {{ user.name }}
</div>

<!-- KeyValuePipe (Objets) -->
<div *ngFor="let item of objet | keyvalue">
  {{ item.key }}: {{ item.value }}
</div>

# === CRÉER UN PIPE PERSONNALISÉ ===

# Générer pipe
ng generate pipe pipes/reverse
ng g p pipes/reverse

# Structure générée:
src/app/pipes/
├── reverse.pipe.ts
└── reverse.pipe.spec.ts

# === PIPE SIMPLE ===

// reverse.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'reverse',
  standalone: true
})
export class ReversePipe implements PipeTransform {
  transform(value: string): string {
    if (!value) return value;
    return value.split('').reverse().join('');
  }
}

// Utilisation
{{ 'Angular' | reverse }}  <!-- ralugnA -->

# === PIPE AVEC PARAMÈTRES ===

// truncate.pipe.ts
@Pipe({
  name: 'truncate',
  standalone: true
})
export class TruncatePipe implements PipeTransform {
  transform(value: string, limit: number = 50, suffix: string = '...'): string {
    if (!value) return value;
    
    if (value.length <= limit) {
      return value;
    }
    
    return value.substring(0, limit) + suffix;
  }
}

// Utilisation
{{ longText | truncate:20 }}              <!-- 20 caractères max -->
{{ longText | truncate:30:'...' }}        <!-- 30 caractères + ... -->

# === PIPE AVEC TABLEAU ===

// filter.pipe.ts
@Pipe({
  name: 'filter',
  standalone: true
})
export class FilterPipe implements PipeTransform {
  transform(items: any[], searchText: string, property: string): any[] {
    if (!items) return [];
    if (!searchText) return items;
    
    searchText = searchText.toLowerCase();
    
    return items.filter(item => {
      return item[property].toLowerCase().includes(searchText);
    });
  }
}

// Utilisation
<input [(ngModel)]="searchTerm" placeholder="Rechercher">
<div *ngFor="let user of users | filter:searchTerm:'name'">
  {{ user.name }}
</div>

# === PIPE IMPUR (Pure: false) ===

// Par défaut, pipes sont "pure" (ne s'exécutent que si input change)
// Pipe "impure" s'exécute à chaque cycle de détection

@Pipe({
  name: 'filterImpure',
  standalone: true,
  pure: false  // Impure pipe (attention aux performances!)
})
export class FilterImpurePipe implements PipeTransform {
  transform(items: any[], callback: (item: any) => boolean): any[] {
    if (!items || !callback) return items;
    return items.filter(callback);
  }
}

// Utilisation
<div *ngFor="let item of items | filterImpure:isActive">
  {{ item.name }}
</div>

# === PIPE ASYNC PERSONNALISÉ ===

// time-ago.pipe.ts
@Pipe({
  name: 'timeAgo',
  standalone: true
})
export class TimeAgoPipe implements PipeTransform {
  transform(value: Date | string): string {
    if (!value) return '';
    
    const date = new Date(value);
    const now = new Date();
    const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
    
    if (seconds < 60) return 'à l\'instant';
    if (seconds < 3600) return `il y a ${Math.floor(seconds / 60)} min`;
    if (seconds < 86400) return `il y a ${Math.floor(seconds / 3600)} h`;
    if (seconds < 604800) return `il y a ${Math.floor(seconds / 86400)} j`;
    
    return date.toLocaleDateString();
  }
}

// Utilisation
{{ post.createdAt | timeAgo }}  <!-- "il y a 5 min" -->

# === CHAÎNER PLUSIEURS PIPES ===

{{ dateActuelle | date:'short' | uppercase }}
{{ prix | currency:'EUR' | uppercase }}
{{ users | filter:searchTerm:'name' | slice:0:10 }}

# === UTILISER PIPE DANS COMPOSANT ===

import { DatePipe } from '@angular/common';

@Component({
  selector: 'app-exemple',
  standalone: true,
  providers: [DatePipe]
})
export class ExempleComponent {
  constructor(private datePipe: DatePipe) {}
  
  formatDate(date: Date): string {
    return this.datePipe.transform(date, 'dd/MM/yyyy') || '';
  }
}


[OK] DIRECTIVES

Les directives modifient le comportement ou l'apparence des éléments DOM.

# Générer directive
ng generate directive directives/highlight
ng g d directives/highlight

# === DIRECTIVE D'ATTRIBUT ===

// highlight.directive.ts
import { Directive, ElementRef, HostListener, Input } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  @Input() appHighlight = 'yellow';
  @Input() defaultColor = 'transparent';
  
  constructor(private el: ElementRef) {}
  
  @HostListener('mouseenter') onMouseEnter() {
    this.highlight(this.appHighlight);
  }
  
  @HostListener('mouseleave') onMouseLeave() {
    this.highlight(this.defaultColor);
  }
  
  private highlight(color: string) {
    this.el.nativeElement.style.backgroundColor = color;
  }
}

// Utilisation
<p appHighlight>Survolez-moi (jaune par défaut)</p>
<p appHighlight="red">Survolez-moi (rouge)</p>
<p [appHighlight]="couleur" defaultColor="lightblue">Survolez-moi</p>

# === DIRECTIVE AVEC RENDERER2 (Recommandé) ===

import { Directive, ElementRef, Renderer2, HostListener } from '@angular/core';

@Directive({
  selector: '[appHighlight]',
  standalone: true
})
export class HighlightDirective {
  constructor(
    private el: ElementRef,
    private renderer: Renderer2
  ) {}
  
  @HostListener('mouseenter') onMouseEnter() {
    this.renderer.setStyle(this.el.nativeElement, 'backgroundColor', 'yellow');
    this.renderer.addClass(this.el.nativeElement, 'highlight');
  }
  
  @HostListener('mouseleave') onMouseLeave() {
    this.renderer.removeStyle(this.el.nativeElement, 'backgroundColor');
    this.renderer.removeClass(this.el.nativeElement, 'highlight');
  }
}

# === DIRECTIVE STRUCTURELLE ===

// unless.directive.ts (inverse de *ngIf)
import { Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core';

@Directive({
  selector: '[appUnless]',
  standalone: true
})
export class UnlessDirective {
  private hasView = false;
  
  constructor(
    private templateRef: TemplateRef<any>,
    private viewContainer: ViewContainerRef
  ) {}
  
  @Input() set appUnless(condition: boolean) {
    if (!condition && !this.hasView) {
      this.viewContainer.createEmbeddedView(this.templateRef);
      this.hasView = true;
    } else if (condition && this.hasView) {
      this.viewContainer.clear();
      this.hasView = false;
    }
  }
}

// Utilisation
<div *appUnless="condition">
  Affiché si condition est false
</div>

# === DIRECTIVE AVEC @HOSTBINDING ===

import { Directive, HostBinding, HostListener } from '@angular/core';

@Directive({
  selector: '[appButton]',
  standalone: true
})
export class ButtonDirective {
  @HostBinding('class.active') isActive = false;
  @HostBinding('style.backgroundColor') bgColor = '#007bff';
  @HostBinding('disabled') isDisabled = false;
  
  @HostListener('click') onClick() {
    this.isActive = !this.isActive;
    this.bgColor = this.isActive ? '#28a745' : '#007bff';
  }
}

// Utilisation
<button appButton>Cliquez-moi</button>

# === DIRECTIVE DE VALIDATION ===

import { Directive } from '@angular/core';
import { NG_VALIDATORS, Validator, AbstractControl, ValidationErrors } from '@angular/forms';

@Directive({
  selector: '[appEmailValidator]',
  standalone: true,
  providers: [{
    provide: NG_VALIDATORS,
    useExisting: EmailValidatorDirective,
    multi: true
  }]
})
export class EmailValidatorDirective implements Validator {
  validate(control: AbstractControl): ValidationErrors | null {
    const email = control.value;
    
    if (!email) {
      return null;
    }
    
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    const valid = regex.test(email);
    
    return valid ? null : { invalidEmail: true };
  }
}

// Utilisation
<input type="text" appEmailValidator [(ngModel)]="email">


[OK] LIFECYCLE HOOKS

Méthodes appelées à différentes étapes du cycle de vie d'un composant.

import { Component, OnInit, OnDestroy, OnChanges, DoCheck, 
         AfterContentInit, AfterContentChecked, AfterViewInit, 
         AfterViewChecked, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-lifecycle',
  standalone: true
})
export class LifecycleComponent implements 
  OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked,
  AfterViewInit, AfterViewChecked, OnDestroy {
  
  // 1. Constructeur
  constructor() {
    console.log('1. Constructor');
  }
  
  // 2. OnChanges - Appelé quand @Input change
  ngOnChanges(changes: SimpleChanges): void {
    console.log('2. OnChanges', changes);
    
    if (changes['titre']) {
      console.log('Titre changé:', 
        changes['titre'].previousValue, 
        '->', 
        changes['titre'].currentValue
      );
    }
  }
  
  // 3. OnInit - Initialisation du composant (une seule fois)
  ngOnInit(): void {
    console.log('3. OnInit - Initialisation');
    // Charger données, initialiser variables, souscrire observables
  }
  
  // 4. DoCheck - Détection de changements personnalisée
  ngDoCheck(): void {
    console.log('4. DoCheck');
    // Attention: appelé très souvent, peut impacter performances
  }
  
  // 5. AfterContentInit - Après projection de contenu (ng-content)
  ngAfterContentInit(): void {
    console.log('5. AfterContentInit');
  }
  
  // 6. AfterContentChecked - Après vérification contenu projeté
  ngAfterContentChecked(): void {
    console.log('6. AfterContentChecked');
  }
  
  // 7. AfterViewInit - Après initialisation de la vue
  ngAfterViewInit(): void {
    console.log('7. AfterViewInit');
    // Accès aux @ViewChild disponible ici
  }
  
  // 8. AfterViewChecked - Après vérification de la vue
  ngAfterViewChecked(): void {
    console.log('8. AfterViewChecked');
  }
  
  // 9. OnDestroy - Avant destruction du composant
  ngOnDestroy(): void {
    console.log('9. OnDestroy - Nettoyage');
    // Unsubscribe, clearInterval, libérer ressources
  }
}

# === ORDRE D'EXÉCUTION ===

# Au montage:
1. Constructor
2. OnChanges (si @Input)
3. OnInit
4. DoCheck
5. AfterContentInit
6. AfterContentChecked
7. AfterViewInit
8. AfterViewChecked

# À chaque changement:
OnChanges (si @Input change)
DoCheck
AfterContentChecked
AfterViewChecked

# À la destruction:
OnDestroy

# === UTILISATIONS COURANTES ===

@Component({
  selector: 'app-exemple',
  standalone: true
})
export class ExempleComponent implements OnInit, OnDestroy {
  private destroy$ = new Subject<void>();
  private intervalId?: number;
  
  // OnInit: Initialisation
  ngOnInit(): void {
    // Charger données
    this.loadData();
    
    // Souscrire à observables
    this.service.data$.pipe(
      takeUntil(this.destroy$)
    ).subscribe(data => {
      this.data = data;
    });
    
    // Démarrer interval
    this.intervalId = window.setInterval(() => {
      this.refresh();
    }, 5000);
  }
  
  // OnDestroy: Nettoyage
  ngOnDestroy(): void {
    // Unsubscribe observables
    this.destroy$.next();
    this.destroy$.complete();
    
    // Clear interval
    if (this.intervalId) {
      clearInterval(this.intervalId);
    }
    
    // Libérer ressources
    this.cleanup();
  }
  
  loadData(): void {
    // Charger données
  }
  
  refresh(): void {
    // Rafraîchir
  }
  
  cleanup(): void {
    // Nettoyer
  }
}


[OK] MODULES (ANCIEN STYLE - NGMODULE)

Note: Les Standalone Components sont maintenant la méthode recommandée (Angular 17+).
Les NgModules restent supportés mais moins utilisés.

# === STRUCTURE NGMODULE ===

// app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';
import { AboutComponent } from './about/about.component';

@NgModule({
  declarations: [      // Composants, directives, pipes du module
    AppComponent,
    HomeComponent,
    AboutComponent
  ],
  imports: [           // Modules à importer
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    FormsModule,
    ReactiveFormsModule
  ],
  providers: [         // Services (DI)
    // Services ici
  ],
  bootstrap: [AppComponent]  // Composant racine
})
export class AppModule { }

# === FEATURE MODULE ===

// users/users.module.ts
@NgModule({
  declarations: [
    UsersListComponent,
    UserDetailComponent
  ],
  imports: [
    CommonModule,
    UsersRoutingModule,
    SharedModule
  ],
  exports: [           // Exporter pour utilisation externe
    UsersListComponent
  ]
})
export class UsersModule { }

# === SHARED MODULE ===

// shared/shared.module.ts
@NgModule({
  declarations: [
    CustomButtonComponent,
    CustomPipe
  ],
  imports: [
    CommonModule
  ],
  exports: [           // Tout export pour réutilisation
    CommonModule,
    CustomButtonComponent,
    CustomPipe
  ]
})
export class SharedModule { }

# === CORE MODULE (Singleton Services) ===

// core/core.module.ts
@NgModule({
  providers: [
    AuthService,
    ApiService
  ]
})
export class CoreModule {
  // S'assurer que CoreModule n'est importé qu'une fois
  constructor(@Optional() @SkipSelf() parentModule?: CoreModule) {
    if (parentModule) {
      throw new Error('CoreModule est déjà chargé. Importer seulement dans AppModule.');
    }
  }
}

# === LAZY LOADING MODULE ===

// app-routing.module.ts
const routes: Routes = [
  {
    path: 'users',
    loadChildren: () => import('./users/users.module').then(m => m.UsersModule)
  }
];


[OK] STANDALONE COMPONENTS (MODERNE - Angular 17+)

La méthode moderne recommandée, sans NgModules.

# === CRÉER STANDALONE APP ===

ng new mon-app --standalone

# === COMPOSANT STANDALONE ===

// app.component.ts
import { Component } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterOutlet } from '@angular/router';
import { MonComposantComponent } from './mon-composant/mon-composant.component';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [
    CommonModule,
    RouterOutlet,
    MonComposantComponent
  ],
  templateUrl: './app.component.html'
})
export class AppComponent {
  title = 'mon-app';
}

# === CONFIGURATION APP ===

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';

export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideHttpClient(),
    // Autres providers
  ]
};

// main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component


[OK] TESTS

# === TESTS UNITAIRES (KARMA + JASMINE) ===

// mon-composant.component.spec.ts
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MonComposantComponent } from './mon-composant.component';

describe('MonComposantComponent', () => {
  let component: MonComposantComponent;
  let fixture: ComponentFixture<MonComposantComponent>;
  
  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [MonComposantComponent]  // Standalone
    }).compileComponents();
    
    fixture = TestBed.createComponent(MonComposantComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });
  
  it('should create', () => {
    expect(component).toBeTruthy();
  });
  
  it('should have a title', () => {
    expect(component.titre).toBe('Mon Composant');
  });
  
  it('should increment counter', () => {
    component.compteur = 0;
    component.incrementer();
    expect(component.compteur).toBe(1);
  });
  
  it('should render title in h1', () => {
    const compiled = fixture.nativeElement as HTMLElement;
    expect(compiled.querySelector('h1')?.textContent).toContain('Mon Composant');
  });
  
  it('should call method on button click', () => {
    spyOn(component, 'incrementer');
    const button = fixture.nativeElement.querySelector('button');
    button.click();
    expect(component.incrementer).toHaveBeenCalled();
  });
});

# === TESTER SERVICE ===

// user.service.spec.ts
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;
  
  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService]
    });
    
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });
  
  afterEach(() => {
    httpMock.verify();
  });
  
  it('should be created', () => {
    expect(service).toBeTruthy();
  });
  
  it('should fetch users', () => {
    const mockUsers = [
      { id: 1, name: 'Alice' },
      { id: 2, name: 'Bob' }
    ];
    
    service.getUsers().subscribe(users => {
      expect(users.length).toBe(2);
      expect(users).toEqual(mockUsers);
    });
    
    const req = httpMock.expectOne('https://api.example.com/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers);
  });
  
  it('should handle error', () => {
    service.getUsers().subscribe({
      next: () => fail('should have failed'),
      error: (error) => {
        expect(error).toBeTruthy();
      }
    });
    
    const req = httpMock.expectOne('https://api.example.com/users');
    req.flush('Error', { status: 500, statusText: 'Server Error' });
  });
});

# === TESTER AVEC MOCK ===

// Mock service
class MockUserService {
  getUsers() {
    return of([{ id: 1, name: 'Test' }]);
  }
}

// Test
beforeEach(async () => {
  await TestBed.configureTestingModule({
    imports: [MonComposantComponent],
    providers: [
      { provide: UserService, useClass: MockUserService }
    ]
  }).compileComponents();
});

# === TESTS E2E (CYPRESS - Recommandé) ===

# Installer Cypress
npm install --save-dev cypress
npx cypress open

// cypress/e2e/app.cy.ts
describe('Application', () => {
  beforeEach(() => {
    cy.visit('/');
  });
  
  it('should display welcome message', () => {
    cy.contains('Welcome to mon-app!');
  });
  
  it('should navigate to about page', () => {
    cy.get('a[routerLink="/about"]').click();
    cy.url().should('include', '/about');
    cy.contains('About Page');
  });
  
  it('should submit form', () => {
    cy.get('input[name="nom"]').type('Alice');
    cy.get('input[name="email"]').type('alice@example.com');
    cy.get('button[type="submit"]').click();
    cy.contains('Formulaire soumis');
  });
});

# Lancer tests
ng test                    # Tests unitaires
npx cypress open          # Tests E2E (interface)
npx cypress run           # Tests E2E (headless)


[OK] DÉPLOIEMENT

# === BUILD PRODUCTION ===

# Build
ng build
ng build --configuration production
ng build --prod                        # Déprécié Angular 12+

# Options de build
ng build --base-href /mon-app/        # Base URL
ng build --output-path dist/custom    # Dossier sortie
ng build --source-map                 # Générer source maps
ng build --optimization=true          # Optimisation
ng build --aot                        # Ahead-of-Time compilation

# Build avec configuration personnalisée
ng build --configuration staging

# Configuration dans angular.json
{
  "projects": {
    "mon-app": {
      "architect": {
        "build": {
          "configurations": {
            "production": {
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": true,
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "5mb"
                }
              ]
            },
            "staging": {
              "optimization": true,
              "sourceMap": true,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.staging.ts"
                }
              ]
            }
          }
        }
      }
    }
  }
}

# === ENVIRONNEMENTS ===

// src/environments/environment.ts (dev)
export const environment = {
  production: false,
  apiUrl: 'http://localhost:3000/api'
};

// src/environments/environment.production.ts
export const environment = {
  production: true,
  apiUrl: 'https://api.monapp.com'
};

// Utilisation
import { environment } from '../environments/environment';

apiUrl = environment.apiUrl;

# === DÉPLOIEMENT NETLIFY ===

# 1. Créer netlify.toml
[build]
  command = "ng build"
  publish = "dist/mon-app/browser"

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

# 2. Déployer
netlify deploy
netlify deploy --prod

# === DÉPLOIEMENT VERCEL ===

# 1. Installer Vercel CLI
npm install -g vercel

# 2. Déployer
vercel

# Configuration vercel.json
{
  "version": 2,
  "builds": [
    {
      "src": "package.json",
      "use": "@vercel/static-build",
      "config": {
        "distDir": "dist/mon-app/browser"
      }
    }
  ],
  "routes": [
    {
      "src": "/(.*)",
      "dest": "/index.html"
    }
  ]
}

# === DÉPLOIEMENT FIREBASE ===

# 1. Installer Firebase CLI
npm install -g firebase-tools

# 2. Initialiser
firebase init hosting

# 3. Configuration firebase.json
{
  "hosting": {
    "public": "dist/mon-app/browser",
    "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

# 4. Déployer
firebase deploy

# === DÉPLOIEMENT GITHUB PAGES ===

# 1. Installer angular-cli-ghpages
npm install -g angular-cli-ghpages

# 2. Build
ng build --base-href /mon-repo/

# 3. Déployer
npx angular-cli-ghpages --dir=dist/mon-app/browser

# === DÉPLOIEMENT DOCKER ===

# Dockerfile
FROM node:20 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist/mon-app/browser /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

# nginx.conf
server {
  listen 80;
  location / {
    root /usr/share/nginx/html;
    index index.html;
    try_files $uri $uri/ /index.html;
  }
}

# Build et run
docker build -t mon-app .
docker run -p 8080:80 mon-app


[OK] BONNES PRATIQUES

# === STRUCTURE DE PROJET ===

src/
├── app/
│   ├── core/                    # Services singleton, guards, interceptors
│   │   ├── services/
│   │   ├── guards/
│   │   └── interceptors/
│   ├── shared/                  # Composants, pipes, directives réutilisables
│   │   ├── components/
│   │   ├── pipes/
│   │   └── directives/
│   ├── features/                # Modules métier
│   │   ├── users/
│   │   ├── products/
│   │   └── dashboard/
│   ├── layout/                  # Layout components (header, footer, sidebar)
│   ├── models/                  # Interfaces et types
│   ├── app.component.ts
│   ├── app.config.ts
│   └── app.routes.ts
├── assets/
├── environments/
└── styles/

# === CONVENTIONS DE NOMMAGE ===

# Fichiers
user.component.ts
user.component.html
user.component.scss
user.component.spec.ts
user.service.ts
user.model.ts
user.routes.ts

# Classes
export class UserComponent { }
export class UserService { }
export interface User { }

# Sélecteurs
@Component({
  selector: 'app-user-list'  // kebab-case avec préfixe
})

# Variables et méthodes
userName: string;           // camelCase
getUserById(id: number) { }

# Constantes
const MAX_USERS = 100;      // UPPER_CASE

# Observables
users$: Observable<User[]>; // Suffixe $

# === PERFORMANCE ===

# 1. OnPush Change Detection
@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})

# 2. TrackBy pour *ngFor
<div *ngFor="let item of items; trackBy: trackByFn">
  {{ item.name }}
</div>

trackByFn(index: number, item: any): any {
  return item.id;
}

# 3. Lazy Loading
{
  path: 'admin',
  loadComponent: () => import('./admin/admin.component')
}

# 4. Unsubscribe des observables
private destroy$ = new Subject<void>();

ngOnInit(): void {
  this.service.data$.pipe(
    takeUntil(this.destroy$)
  ).subscribe();
}

ngOnDestroy(): void {
  this.destroy$.next();
  this.destroy$.complete();
}

# 5. Pure Pipes
@Pipe({
  name: 'myPipe',
  pure: true  // Défaut
})

# 6. Async Pipe dans templates
users$ = this.service.getUsers();

<div *ngFor="let user of users$ | async">
  {{ user.name }}
</div>

# === SÉCURITÉ ===

# 1. Éviter innerHTML
<!-- Dangereux -->
<div [innerHTML]="userInput"></div>

<!-- Sûr -->
<div>{{ userInput }}</div>

# 2. Sanitizer si nécessaire
import { DomSanitizer } from '@angular/platform-browser';

constructor(private sanitizer: DomSanitizer) {}

getSafeHtml(html: string) {
  return this.sanitizer.sanitize(SecurityContext.HTML, html);
}

# 3. CSRF Protection (activé par défaut)
import { provideHttpClient, withXsrfConfiguration } from '@angular/common/http';

providers: [
  provideHttpClient(
    withXsrfConfiguration({
      cookieName: 'XSRF-TOKEN',
      headerName: 'X-XSRF-TOKEN'
    })
  )
]

# 4. Content Security Policy
<!-- index.html -->
<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; script-src 'self'">

# === ACCESSIBILITÉ ===

# Attributs ARIA
<button aria-label="Fermer" (click)="close()">×</button>
<nav aria-label="Navigation principale">
<div role="alert" aria-live="polite">Message</div>

# Keyboard navigation
<button (keydown.enter)="submit()" (keydown.space)="submit()">

# Focus management
@ViewChild('input') inputRef!: ElementRef;

ngAfterViewInit() {
  this.inputRef.nativeElement.focus();
}

# === INTERNATIONALISATION (i18n) ===

# 1. Marquer textes
<h1 i18n>Hello</h1>
<p i18n="@@welcomeMessage">Welcome to our app</p>
<img [src]="logo" i18n-alt alt="Company logo">

# 2. Extraire traductions
ng extract-i18n

# 3. Fichiers générés
messages.xlf      # Anglais
messages.fr.xlf   # Français

# 4. Build avec locale
ng build --localize
ng build --configuration=fr

# 5. Configuration angular.json
"i18n": {
  "sourceLocale": "en",
  "locales": {
    "fr": "src/locale/messages.fr.xlf"
  }
}

# === DEBUGGING ===

# Angular DevTools (Chrome Extension)
# https://chrome.google.com/webstore/detail/angular-devtools

# Console
console.log(this.variable);
console.table(this.array);
debugger;  // Breakpoint

# Source Maps en production
ng build --source-map

# Augury (ancien, moins maintenu)
# https://augury.rangle.io/


[OK] RESSOURCES

# Documentation officielle
https://angular.dev/
https://angular.io/docs

# API Reference
https://angular.io/api

# Style Guide
https://angular.io/guide/styleguide

# Angular Blog
https://blog.angular.io/

# GitHub
https://github.com/angular/angular

# Tutoriels
https://angular.io/tutorial
https://www.angular.love/

# Communauté
https://stackoverflow.com/questions/tagged/angular
https://www.reddit.com/r/Angular2/

# Cours
https://www.udemy.com/topic/angular/
https://www.pluralsight.com/paths/angular

# Newsletters
https://angular.love/newsletter
https://blog.angular.io/

# Outils
https://stackblitz.com/       # IDE en ligne
https://angular.io/cli        # CLI
https://material.angular.io/  # Material Design
https://ng-bootstrap.github.io/  # Bootstrap
https://www.primefaces.org/primeng/  # PrimeNG


[OK] ÉCOSYSTÈME & LIBRAIRIES POPULAIRES

# UI Components
npm install @angular/material @angular/cdk
npm install @ng-bootstrap/ng-bootstrap
npm install primeng primeicons
npm install @progress/kendo-angular-ui

# State Management
npm install @ngrx/store @ngrx/effects @ngrx/entity
npm install @ngxs/store
npm install akita

# Forms
npm install @angular/forms
npm install @ngneat/reactive-forms

# Icons
npm install @angular/material-icons
npm install font-awesome
npm install lucide-angular

# Charts
npm install ng2-charts chart.js
npm install @swimlane/ngx-charts
npm install plotly.js-angular-dist

# Maps
npm install @angular/google-maps
npm install leaflet @asymmetrik/ngx-leaflet

# Utilitaires
npm install lodash-es @types/lodash-es
npm install date-fns
npm install rxjs

# Testing
npm install @angular/core/testing
npm install cypress
npm install @testing-library/angular

# PWA
ng add @angular/pwa

# SSR (Server-Side Rendering)
ng add @angular/ssr


[OK] COMMANDES CLI COMPLÈTES

# Aide
ng help
ng <command> --help
ng generate --help

# Créer
ng new <name>
ng generate component <name>
ng generate service <name>
ng generate module <name>
ng generate directive <name>
ng generate pipe <name>
ng generate guard <name>
ng generate interface <name>
ng generate class <name>
ng generate enum <name>

# Raccourcis
ng g c <name>
ng g s <name>
ng g m <name>
ng g d <name>
ng g p <name>
ng g g <name>
ng g i <name>
ng g cl <name>
ng g e <name>

# Servir
ng serve
ng s
ng serve --open
ng serve --port 4300
ng serve --ssl

# Build
ng build
ng build --configuration production
ng build --watch

# Test
ng test
ng test --watch=false
ng test --code-coverage
ng e2e

# Lint
ng lint
ng lint --fix

# Update
ng update
ng update @angular/cli @angular/core
ng update --all

# Add
ng add @angular/material
ng add @angular/pwa
ng add @angular/ssr

# Version
ng version
ng v

# Configuration
ng config
ng config cli.packageManager yarn


[OK] ERREURS COURANTES & SOLUTIONS

# === Erreur: Can't bind to 'ngModel' ===
# Solution: Importer FormsModule
import { FormsModule } from '@angular/forms';

@Component({
  imports: [FormsModule]
})

# === Erreur: No provider for HttpClient ===
# Solution: Ajouter provideHttpClient
import { provideHttpClient } from '@angular/common/http';

export const appConfig: ApplicationConfig = {
  providers: [provideHttpClient()]
};

# === Erreur: Cannot find module ===
# Solution: Vérifier imports
npm install
npm install --save <package>

# === Erreur: ExpressionChangedAfterItHasBeenCheckedError ===
# Solution: Utiliser setTimeout ou ChangeDetectorRef
constructor(private cdr: ChangeDetectorRef) {}

ngAfterViewInit() {
  setTimeout(() => {
    this.variable = value;
  });
  // OU
  this.variable = value;
  this.cdr.detectChanges();
}

# === Erreur: Circular dependency ===
# Solution: Restructurer imports ou utiliser forwardRef

# === Erreur: Memory leak (subscription) ===
# Solution: Unsubscribe
private destroy$ = new Subject<void>();

ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
}

# === Port déjà utilisé ===
# Solution: Changer port ou tuer processus
ng serve --port 4300
# OU
lsof -ti:4200 | xargs kill -9  # Mac/Linux
netstat -ano | findstr :4200   # Windows


[OK] ASTUCES & TIPS

# === Génération rapide ===
# Composant inline (sans dossier)
ng g c mon-composant --flat --inline-template --inline-style --skip-tests

# === Debugging dans template ===
<pre>{{ variable | json }}</pre>
{{ variable | json }}

# === Console dans template ===
{{ log(variable) }}

// Composant
log(value: any) {
  console.log(value);
  return value;
}

# === Espionner changements ===
ngDoCheck() {
  console.log('Change detection');
}

# === Performance profiling ===
ng build --stats-json
webpack-bundle-analyzer dist/stats.json

# === Raccourcis VSCode ===
Ctrl+Space      # Autocomplétion
F12             # Aller à la définition
Shift+F12       # Trouver références
F2              # Renommer
Ctrl+.          # Quick fix

# === Extensions VSCode recommandées ===
- Angular Language Service
- Angular Snippets
- Prettier
- ESLint
- GitLens
- Auto Rename Tag
- Path Intellisense';
import { appConfig } from './app/app.config';

bootstrapApplication(AppComponent, appConfig);

// app.component.ts
import { Component } from '@angular/core';
import { RouterOutlet, RouterLink } from '@angular/router';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [RouterOutlet, RouterLink],
  template: `
    <nav>
      <a routerLink="/">Accueil</a>
      <a routerLink="/about">À propos</a>
      <a routerLink="/contact">Contact</a>
    </nav>
    <router-outlet></router-outlet>
  `
})
export class AppComponent {}

# === TYPES DE ROUTES ===

export const routes: Routes = [
  // Route simple
  { path: 'home', component: HomeComponent },
  
  // Route avec paramètre
  { path: 'user/:id', component: UserComponent },
  
  // Route avec plusieurs paramètres
  { path: 'posts/:category/:id', component: PostComponent },
  
  // Route avec query params
  { path: 'search', component: SearchComponent },
  // URL: /search?q=angular&page=1
  
  // Redirection
  { path: 'old-path', redirectTo: 'new-path', pathMatch: 'full' },
  
  // Route avec enfants (nested routes)
  {
    path: 'dashboard',
    component: DashboardComponent,
    children: [
      { path: '', component: DashboardHomeComponent },
      { path: 'profile', component: ProfileComponent },
      { path: 'settings', component: SettingsComponent }
    ]
  },
  
  // Lazy loading
  {
    path: 'admin',
    loadComponent: () => import('./admin/admin.component')
      .then(m => m.AdminComponent)
  },
  
  // Lazy loading module complet
  {
    path: 'products',
    loadChildren: () => import('./products/products.routes')
      .then(m => m.PRODUCTS_ROUTES)
  },
  
  // Route avec guard (protection)
  {
    path: 'protected',
    component: ProtectedComponent,
    canActivate: [authGuard]
  },
  
  // Route 404
  { path: '**', component: NotFoundComponent }
];

# === NAVIGATION DANS TEMPLATE ===

<!-- Liens simples -->
<a routerLink="/">Accueil</a>
<a routerLink="/about">À propos</a>
<a [routerLink]="['/user', userId]">Profil</a>

<!-- Avec classe active -->
<a routerLink="/about" routerLinkActive="active">À propos</a>
<a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
  Accueil
</a>

<!-- Navigation avec paramètres -->
<a [routerLink]="['/user', 123]">User 123</a>
<a [routerLink]="['/posts', 'tech', 456]">Post</a>

<!-- Navigation avec query params -->
<a [routerLink]="['/search']" [queryParams]="{q: 'angular', page: 1}">
  Rechercher
</a>

<!-- Navigation avec fragment (ancre) -->
<a [routerLink]="['/about']" fragment="section2">Section 2</a>

<!-- Navigation relative -->
<a [routerLink]="['./details']">Détails</a>
<a [routerLink]="['../sibling']">Sibling</a>

# === NAVIGATION PROGRAMMATIQUE ===

import { Component } from '@angular/core';
import { Router, ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-exemple',
  standalone: true
})
export class ExempleComponent {
  constructor(
    private router: Router,
    private route: ActivatedRoute
  ) {}
  
  // Navigation simple
  naviguerVersAbout(): void {
    this.router.navigate(['/about']);
  }
  
  // Navigation avec paramètres
  naviguerVersUser(id: number): void {
    this.router.navigate(['/user', id]);
  }
  
  // Navigation avec query params
  rechercher(query: string): void {
    this.router.navigate(['/search'], {
      queryParams: { q: query, page: 1 }
    });
  }
  
  // Navigation avec fragment
  naviguerVersSection(): void {
    this.router.navigate(['/about'], {
      fragment: 'section2'
    });
  }
  
  // Navigation relative
  naviguerRelatif(): void {
    this.router.navigate(['../autre'], { relativeTo: this.route });
  }
  
  // Navigation avec remplacement historique
  remplacer(): void {
    this.router.navigate(['/new'], {
      replaceUrl: true  // Ne pas ajouter à l'historique
    });
  }
  
  // Retour arrière
  retour(): void {
    window.history.back();
  }
  
  // NavigateByUrl (URL complète)
  naviguerUrl(): void {
    this.router.navigateByUrl('/about?page=1#section');
  }
}

# === LIRE PARAMÈTRES DE ROUTE ===

import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router';

@Component({
  selector: 'app-user',
  standalone: true,
  template: `
    <h1>User {{ userId }}</h1>
    <p>Category: {{ category }}</p>
    <p>Search: {{ searchQuery }}</p>
  `
})
export class UserComponent implements OnInit {
  userId: string = '';
  category: string = '';
  searchQuery: string = '';
  
  constructor(private route: ActivatedRoute) {}
  
  ngOnInit(): void {
    // Méthode 1: Snapshot (valeur à l'instant T)
    this.userId = this.route.snapshot.paramMap.get('id') || '';
    this.category = this.route.snapshot.queryParamMap.get('category') || '';
    
    // Méthode 2: Observable (réactif aux changements)
    this.route.paramMap.subscribe((params: ParamMap) => {
      this.userId = params.get('id') || '';
    });
    
    // Query parameters
    this.route.queryParamMap.subscribe(params => {
      this.searchQuery = params.get('q') || '';
    });
    
    // Tous les query params
    this.route.queryParams.subscribe(params => {
      console.log('All params:', params);
      // { q: 'angular', page: '1' }
    });
    
    // Fragment (ancre)
    this.route.fragment.subscribe(fragment => {
      console.log('Fragment:', fragment);
    });
    
    // Data (données statiques de route)
    this.route.data.subscribe(data => {
      console.log('Route data:', data);
    });
  }
}

# === ROUTES AVEC DATA ===

export const routes: Routes = [
  {
    path: 'about',
    component: AboutComponent,
    data: { title: 'À propos', breadcrumb: 'About' }
  }
];

// Lire dans composant
this.route.data.subscribe(data => {
  document.title = data['title'];
});

# === GUARDS (PROTECTION DE ROUTES) ===

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

# Types de guards:
# - CanActivate: Peut-on accéder à la route?
# - CanActivateChild: Peut-on accéder aux routes enfants?
# - CanDeactivate: Peut-on quitter la route?
# - CanLoad: Peut-on charger le module?
# - Resolve: Pré-charger des données

# === CanActivate Guard ===

// auth.guard.ts
import { inject } from '@angular/core';
import { Router, CanActivateFn } from '@angular/router';
import { AuthService } from '../services/auth.service';

export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);
  
  if (authService.estConnecte()) {
    return true;
  } else {
    router.navigate(['/login'], {
      queryParams: { returnUrl: state.url }
    });
    return false;
  }
};

// Utilisation
export const routes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [authGuard]
  }
];

# === CanDeactivate Guard ===

// can-deactivate.guard.ts
import { CanDeactivateFn } from '@angular/router';

export interface CanComponentDeactivate {
  canDeactivate: () => boolean | Promise<boolean>;
}

export const canDeactivateGuard: CanDeactivateFn<CanComponentDeactivate> = 
  (component) => {
    return component.canDeactivate ? component.canDeactivate() : true;
  };

// form.component.ts
@Component({
  selector: 'app-form',
  standalone: true
})
export class FormComponent implements CanComponentDeactivate {
  formulaireModifie = false;
  
  canDeactivate(): boolean {
    if (this.formulaireModifie) {
      return confirm('Voulez-vous vraiment quitter? Modifications non sauvegardées.');
    }
    return true;
  }
}

// Route
{
  path: 'form',
  component: FormComponent,
  canDeactivate: [canDeactivateGuard]
}

# === Resolve Guard (Pré-chargement) ===

// user-resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { UserService } from '../services/user.service';

export const userResolver: ResolveFn<User> = (route, state) => {
  const userService = inject(UserService);
  const id = route.paramMap.get('id')!;
  return userService.getUser(+id);
};

// Route
{
  path: 'user/:id',
  component: UserComponent,
  resolve: { user: userResolver }
}

// Composant
ngOnInit(): void {
  this.route.data.subscribe(data => {
    this.user = data['user'];  // Données déjà chargées
  });
}