Top 25 questions d'entretien Angular : guide complet pour réussir

Les 25 questions d'entretien Angular les plus posées en 2026. Réponses détaillées, exemples de code et conseils pour décrocher votre poste de développeur Angular.

Illustration de questions d'entretien Angular avec des composants et services interconnectés

Les entretiens techniques Angular évaluent la compréhension de l'architecture du framework, de TypeScript, et des bonnes pratiques de développement frontend. Ce guide présente les 25 questions les plus fréquentes avec des réponses détaillées et des exemples de code pour une préparation optimale.

Conseil de préparation

Ces questions couvrent les versions récentes d'Angular (16+), incluant les Signals, les standalone components et le nouveau control flow. Maîtriser ces concepts modernes démontre une veille technologique active.

Fondamentaux Angular

1. Quelle est la différence entre Angular et AngularJS ?

Angular (versions 2+) est une réécriture complète d'AngularJS. Les différences majeures concernent l'architecture, le langage et les performances.

AngularJS utilisait JavaScript et le pattern MVC avec un système de two-way binding qui pouvait causer des problèmes de performance. Angular utilise TypeScript, une architecture basée sur les composants, et un système de détection de changement optimisé.

AngularJS (1.x) - Controller-basedtypescript
// angular.module('app').controller('UserController', function($scope) {
//   $scope.user = { name: 'Alice' };
// });

// Angular (2+) - Component-based
// user.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-user',
  standalone: true,
  template: `
    <div class="user-card">
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
    </div>
  `
})
export class UserComponent {
  // Typage fort avec TypeScript
  user = {
    name: 'Alice',
    email: 'alice@example.com'
  };
}

Angular apporte également le support natif des modules ES6, un meilleur outillage avec Angular CLI, et une architecture plus adaptée aux applications d'entreprise.

2. Qu'est-ce qu'un composant Angular et comment le créer ?

Un composant Angular est une classe TypeScript décorée avec @Component. Il encapsule la logique, le template et les styles d'une partie de l'interface utilisateur.

product-card.component.tstypescript
import { Component, Input, Output, EventEmitter } from '@angular/core';
import { CommonModule } from '@angular/common';

// Interface pour le typage fort
interface Product {
  id: number;
  name: string;
  price: number;
  inStock: boolean;
}

@Component({
  // Sélecteur CSS pour utiliser le composant
  selector: 'app-product-card',
  // Standalone = pas besoin de NgModule
  standalone: true,
  // Imports des dépendances
  imports: [CommonModule],
  // Template inline ou externe (templateUrl)
  template: `
    <article class="product-card">
      <h3>{{ product.name }}</h3>
      <p class="price">{{ product.price | currency:'EUR' }}</p>

      @if (product.inStock) {
        <button (click)="onAddToCart()">Ajouter au panier</button>
      } @else {
        <span class="out-of-stock">Rupture de stock</span>
      }
    </article>
  `,
  // Styles encapsulés par défaut
  styles: [`
    .product-card { padding: 1rem; border: 1px solid #e0e0e0; }
    .price { font-weight: bold; color: #2563eb; }
    .out-of-stock { color: #dc2626; }
  `]
})
export class ProductCardComponent {
  // Input : données du parent
  @Input({ required: true }) product!: Product;

  // Output : événements vers le parent
  @Output() addToCart = new EventEmitter<number>();

  onAddToCart() {
    this.addToCart.emit(this.product.id);
  }
}

Les composants standalone (Angular 14+) simplifient la création en éliminant le besoin de déclarer le composant dans un NgModule.

3. Expliquez le cycle de vie d'un composant Angular

Angular fournit des hooks de cycle de vie permettant d'exécuter du code à des moments précis de la vie d'un composant.

lifecycle-demo.component.tstypescript
import {
  Component,
  OnInit,
  OnChanges,
  DoCheck,
  AfterContentInit,
  AfterContentChecked,
  AfterViewInit,
  AfterViewChecked,
  OnDestroy,
  Input,
  SimpleChanges
} from '@angular/core';

@Component({
  selector: 'app-lifecycle-demo',
  standalone: true,
  template: `<p>{{ data }}</p>`
})
export class LifecycleDemoComponent implements
  OnInit, OnChanges, DoCheck, AfterContentInit,
  AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {

  @Input() data = '';

  // 1. Appelé quand un @Input change (avant ngOnInit)
  ngOnChanges(changes: SimpleChanges) {
    console.log('ngOnChanges', changes);
  }

  // 2. Appelé une fois après le premier ngOnChanges
  // Idéal pour les initialisations
  ngOnInit() {
    console.log('ngOnInit - Initialisation du composant');
  }

  // 3. Appelé à chaque cycle de détection de changement
  // À utiliser avec précaution (performance)
  ngDoCheck() {
    console.log('ngDoCheck');
  }

  // 4. Après projection du contenu (ng-content)
  ngAfterContentInit() {
    console.log('ngAfterContentInit');
  }

  // 5. Après chaque vérification du contenu projeté
  ngAfterContentChecked() {
    console.log('ngAfterContentChecked');
  }

  // 6. Après initialisation de la vue du composant
  // Les @ViewChild sont disponibles ici
  ngAfterViewInit() {
    console.log('ngAfterViewInit - Vue initialisée');
  }

  // 7. Après chaque vérification de la vue
  ngAfterViewChecked() {
    console.log('ngAfterViewChecked');
  }

  // 8. Juste avant la destruction du composant
  // Nettoyage : unsubscribe, clearInterval, etc.
  ngOnDestroy() {
    console.log('ngOnDestroy - Nettoyage');
  }
}

Les hooks les plus utilisés sont ngOnInit pour l'initialisation, ngOnChanges pour réagir aux changements d'inputs, et ngOnDestroy pour le nettoyage des ressources.

4. Qu'est-ce que le Data Binding dans Angular ?

Le data binding connecte les données du composant au template. Angular propose quatre formes de binding.

data-binding.component.tstypescript
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';

@Component({
  selector: 'app-data-binding',
  standalone: true,
  imports: [FormsModule],
  template: `
    <!-- 1. Interpolation : composant → template -->
    <h1>{{ title }}</h1>
    <p>{{ getFullName() }}</p>

    <!-- 2. Property Binding : composant → propriété DOM -->
    <img [src]="imageUrl" [alt]="imageAlt">
    <button [disabled]="isLoading">Envoyer</button>

    <!-- 3. Event Binding : template → composant -->
    <button (click)="handleClick()">Cliquer</button>
    <input (keyup.enter)="onEnter($event)">

    <!-- 4. Two-way Binding : bidirectionnel -->
    <input [(ngModel)]="username">
    <p>Bonjour, {{ username }}</p>
  `
})
export class DataBindingComponent {
  // Propriétés pour l'interpolation
  title = 'Mon Application';
  firstName = 'Jean';
  lastName = 'Dupont';

  // Propriétés pour le property binding
  imageUrl = '/assets/logo.png';
  imageAlt = 'Logo de l\'application';
  isLoading = false;

  // Propriété pour le two-way binding
  username = '';

  getFullName(): string {
    return `${this.firstName} ${this.lastName}`;
  }

  handleClick(): void {
    console.log('Bouton cliqué');
  }

  onEnter(event: KeyboardEvent): void {
    const target = event.target as HTMLInputElement;
    console.log('Valeur saisie:', target.value);
  }
}

Le two-way binding [(ngModel)] est une combinaison du property binding et de l'event binding, permettant une synchronisation automatique entre le modèle et la vue.

5. Quelle est la différence entre un Module et un Composant Standalone ?

Les NgModules regroupent des composants, directives et services liés. Les standalone components (Angular 14+) permettent de créer des composants autonomes sans module.

typescript
// Approche traditionnelle avec NgModule
// products.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductListComponent } from './product-list.component';
import { ProductCardComponent } from './product-card.component';
import { ProductService } from './product.service';

@NgModule({
  // Composants appartenant à ce module
  declarations: [
    ProductListComponent,
    ProductCardComponent
  ],
  // Modules dont on a besoin
  imports: [CommonModule],
  // Composants utilisables à l'extérieur
  exports: [ProductListComponent],
  // Services avec scope module
  providers: [ProductService]
})
export class ProductsModule {}

// Approche moderne avec Standalone Components
// product-list.component.ts
import { Component, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ProductCardComponent } from './product-card.component';
import { ProductService } from './product.service';

@Component({
  selector: 'app-product-list',
  // Pas besoin de NgModule
  standalone: true,
  // Imports directs des dépendances
  imports: [CommonModule, ProductCardComponent],
  template: `
    @for (product of products(); track product.id) {
      <app-product-card [product]="product" />
    }
  `
})
export class ProductListComponent {
  // Injection moderne avec inject()
  private productService = inject(ProductService);

  products = this.productService.getProducts();
}

Les standalone components réduisent la complexité, améliorent le tree-shaking et simplifient le lazy loading. Cette approche est recommandée pour les nouveaux projets.

Services et Injection de dépendances

6. Comment fonctionne l'injection de dépendances dans Angular ?

L'injection de dépendances (DI) est un design pattern central dans Angular. Le framework gère la création et la fourniture des instances de services.

user.service.tstypescript
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable, BehaviorSubject } from 'rxjs';

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

// providedIn: 'root' = singleton au niveau application
@Injectable({ providedIn: 'root' })
export class UserService {
  // Injection moderne avec inject()
  private http = inject(HttpClient);

  // État réactif partagé
  private currentUser$ = new BehaviorSubject<User | null>(null);

  // API publique
  getUsers(): Observable<User[]> {
    return this.http.get<User[]>('/api/users');
  }

  getUserById(id: number): Observable<User> {
    return this.http.get<User>(`/api/users/${id}`);
  }

  setCurrentUser(user: User): void {
    this.currentUser$.next(user);
  }

  getCurrentUser(): Observable<User | null> {
    return this.currentUser$.asObservable();
  }
}

// Utilisation dans un composant
// user-profile.component.ts
@Component({
  selector: 'app-user-profile',
  standalone: true,
  template: `
    @if (user$ | async; as user) {
      <h1>{{ user.name }}</h1>
      <p>{{ user.email }}</p>
    }
  `
})
export class UserProfileComponent implements OnInit {
  // Injection via inject() (recommandé)
  private userService = inject(UserService);

  user$!: Observable<User | null>;

  ngOnInit() {
    this.user$ = this.userService.getCurrentUser();
  }
}

Les différents niveaux de provision (providedIn: 'root', au niveau module, ou au niveau composant) permettent de contrôler la portée et le cycle de vie des services.

7. Quelle est la différence entre providedIn root, any et platform ?

Les options de providedIn contrôlent comment Angular crée et partage les instances de service.

1. providedIn: 'root' - Singleton au niveau applicationtypescript
// Une seule instance partagée par toute l'application
@Injectable({ providedIn: 'root' })
export class AuthService {
  private isAuthenticated = false;

  login() { this.isAuthenticated = true; }
  logout() { this.isAuthenticated = false; }
  isLoggedIn() { return this.isAuthenticated; }
}

// 2. providedIn: 'any' - Instance par module lazy-loaded
// Chaque module lazy-loaded obtient sa propre instance
@Injectable({ providedIn: 'any' })
export class FeatureLoggerService {
  private logs: string[] = [];

  log(message: string) {
    this.logs.push(`[${new Date().toISOString()}] ${message}`);
  }
}

// 3. providedIn: 'platform' - Partagé entre applications
// Utile pour micro-frontends ou applications multi-apps
@Injectable({ providedIn: 'platform' })
export class SharedConfigService {
  readonly apiUrl = 'https://api.example.com';
}

// 4. Provision au niveau composant - Instance par composant
@Component({
  selector: 'app-editor',
  standalone: true,
  // Chaque instance du composant a sa propre instance du service
  providers: [EditorStateService],
  template: `...`
})
export class EditorComponent {
  private editorState = inject(EditorStateService);
}

Pour la plupart des cas, providedIn: 'root' est suffisant et optimal pour le tree-shaking.

Tree-shaking des services

Avec providedIn: 'root', Angular peut éliminer les services non utilisés du bundle final. Cela améliore les performances de chargement.

8. Comment utiliser les Observables avec RxJS dans Angular ?

RxJS est la bibliothèque de programmation réactive utilisée par Angular pour gérer les flux de données asynchrones.

search.service.tstypescript
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import {
  Observable,
  Subject,
  debounceTime,
  distinctUntilChanged,
  switchMap,
  catchError,
  of,
  map,
  tap
} from 'rxjs';

interface SearchResult {
  id: number;
  title: string;
  description: string;
}

@Injectable({ providedIn: 'root' })
export class SearchService {
  private http = inject(HttpClient);

  search(term: string): Observable<SearchResult[]> {
    return this.http.get<SearchResult[]>(`/api/search?q=${term}`);
  }
}

// search.component.ts
@Component({
  selector: 'app-search',
  standalone: true,
  imports: [CommonModule, ReactiveFormsModule],
  template: `
    <input [formControl]="searchControl" placeholder="Rechercher...">

    @if (isLoading) {
      <div class="loading">Chargement...</div>
    }

    <ul>
      @for (result of results$ | async; track result.id) {
        <li>{{ result.title }}</li>
      }
    </ul>
  `
})
export class SearchComponent implements OnInit, OnDestroy {
  private searchService = inject(SearchService);
  private destroy$ = new Subject<void>();

  searchControl = new FormControl('');
  results$!: Observable<SearchResult[]>;
  isLoading = false;

  ngOnInit() {
    this.results$ = this.searchControl.valueChanges.pipe(
      // Attendre 300ms après la dernière frappe
      debounceTime(300),
      // Ignorer si la valeur n'a pas changé
      distinctUntilChanged(),
      // Afficher le loading
      tap(() => this.isLoading = true),
      // Annuler la requête précédente et lancer la nouvelle
      switchMap(term =>
        term ? this.searchService.search(term) : of([])
      ),
      // Masquer le loading
      tap(() => this.isLoading = false),
      // Gérer les erreurs
      catchError(error => {
        console.error('Erreur de recherche:', error);
        this.isLoading = false;
        return of([]);
      })
    );
  }

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

Les opérateurs RxJS comme debounceTime, distinctUntilChanged et switchMap sont essentiels pour optimiser les recherches et éviter les requêtes inutiles.

Angular moderne (16+)

9. Que sont les Signals dans Angular et comment les utiliser ?

Les Signals (Angular 16+) introduisent une nouvelle primitive réactive plus simple et performante que les Observables pour l'état local.

counter.component.tstypescript
import { Component, signal, computed, effect } from '@angular/core';

@Component({
  selector: 'app-counter',
  standalone: true,
  template: `
    <div class="counter">
      <h2>Compteur: {{ count() }}</h2>
      <p>Double: {{ doubleCount() }}</p>
      <p>Message: {{ message() }}</p>

      <button (click)="increment()">+1</button>
      <button (click)="decrement()">-1</button>
      <button (click)="reset()">Reset</button>
    </div>
  `
})
export class CounterComponent {
  // Signal writable - valeur modifiable
  count = signal(0);

  // Signal computed - dérivé automatiquement
  // Recalculé uniquement quand count() change
  doubleCount = computed(() => this.count() * 2);

  // Computed avec logique conditionnelle
  message = computed(() => {
    const value = this.count();
    if (value < 0) return 'Valeur négative';
    if (value === 0) return 'Zéro';
    if (value < 10) return 'Petit nombre';
    return 'Grand nombre';
  });

  constructor() {
    // Effect - exécuté à chaque changement des signaux utilisés
    // Utile pour les side effects (logs, localStorage, etc.)
    effect(() => {
      console.log(`Nouvelle valeur: ${this.count()}`);
      localStorage.setItem('counter', String(this.count()));
    });
  }

  increment() {
    // update() pour modifier basé sur la valeur précédente
    this.count.update(value => value + 1);
  }

  decrement() {
    this.count.update(value => value - 1);
  }

  reset() {
    // set() pour définir une valeur directement
    this.count.set(0);
  }
}

Les Signals offrent une meilleure performance grâce à une détection de changement plus fine et une API plus intuitive que RxJS pour l'état local.

10. Comment fonctionnent les Signal Inputs et Outputs ?

Angular 17+ introduit input() et output() comme alternatives signal-based aux décorateurs @Input() et @Output().

task-item.component.tstypescript
import { Component, input, output, computed } from '@angular/core';

interface Task {
  id: number;
  title: string;
  completed: boolean;
  priority: 'low' | 'medium' | 'high';
}

@Component({
  selector: 'app-task-item',
  standalone: true,
  template: `
    <div class="task" [class.completed]="task().completed">
      <input
        type="checkbox"
        [checked]="task().completed"
        (change)="onToggle()"
      >

      <span class="title">{{ task().title }}</span>
      <span class="priority" [class]="priorityClass()">
        {{ task().priority }}
      </span>

      @if (showActions()) {
        <button (click)="onDelete()">Supprimer</button>
      }
    </div>
  `
})
export class TaskItemComponent {
  // Input requis - doit être fourni par le parent
  task = input.required<Task>();

  // Input optionnel avec valeur par défaut
  showActions = input(true);

  // Output basé sur les signaux
  toggle = output<number>();
  delete = output<number>();

  // Computed basé sur l'input
  priorityClass = computed(() => `priority-${this.task().priority}`);

  onToggle() {
    this.toggle.emit(this.task().id);
  }

  onDelete() {
    this.delete.emit(this.task().id);
  }
}

// Utilisation dans le parent
// task-list.component.ts
@Component({
  selector: 'app-task-list',
  standalone: true,
  imports: [TaskItemComponent],
  template: `
    @for (task of tasks(); track task.id) {
      <app-task-item
        [task]="task"
        [showActions]="canEdit()"
        (toggle)="onToggleTask($event)"
        (delete)="onDeleteTask($event)"
      />
    }
  `
})
export class TaskListComponent {
  tasks = signal<Task[]>([
    { id: 1, title: 'Apprendre Angular', completed: false, priority: 'high' },
    { id: 2, title: 'Créer un projet', completed: false, priority: 'medium' }
  ]);

  canEdit = signal(true);

  onToggleTask(id: number) {
    this.tasks.update(tasks =>
      tasks.map(t => t.id === id ? { ...t, completed: !t.completed } : t)
    );
  }

  onDeleteTask(id: number) {
    this.tasks.update(tasks => tasks.filter(t => t.id !== id));
  }
}

Les signal inputs offrent un meilleur typage, une API plus cohérente avec les autres signaux, et préparent la transition vers la détection de changement zoneless.

Prêt à réussir tes entretiens Angular ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

11. Expliquez le nouveau Control Flow d'Angular 17+

Angular 17 introduit une nouvelle syntaxe de control flow intégrée au template, remplaçant les directives structurelles *ngIf, *ngFor et *ngSwitch.

modern-control-flow.component.tstypescript
import { Component, signal } from '@angular/core';

interface User {
  id: number;
  name: string;
  role: 'admin' | 'user' | 'guest';
  status: 'active' | 'inactive' | 'pending';
}

@Component({
  selector: 'app-modern-control-flow',
  standalone: true,
  template: `
    <!-- @if remplace *ngIf -->
    @if (isLoading()) {
      <div class="loading">Chargement en cours...</div>
    } @else if (error()) {
      <div class="error">{{ error() }}</div>
    } @else {
      <div class="content">
        <h2>Utilisateurs ({{ users().length }})</h2>

        <!-- @for remplace *ngFor -->
        <!-- track est obligatoire pour les performances -->
        @for (user of users(); track user.id; let i = $index, first = $first, last = $last) {
          <div class="user" [class.first]="first" [class.last]="last">
            <span class="index">{{ i + 1 }}.</span>
            <span class="name">{{ user.name }}</span>

            <!-- @switch remplace *ngSwitch -->
            @switch (user.role) {
              @case ('admin') {
                <span class="badge admin">Administrateur</span>
              }
              @case ('user') {
                <span class="badge user">Utilisateur</span>
              }
              @default {
                <span class="badge guest">Invité</span>
              }
            }
          </div>
        } @empty {
          <!-- Bloc affiché si la collection est vide -->
          <p>Aucun utilisateur trouvé</p>
        }
      </div>
    }
  `
})
export class ModernControlFlowComponent {
  isLoading = signal(false);
  error = signal<string | null>(null);
  users = signal<User[]>([
    { id: 1, name: 'Alice', role: 'admin', status: 'active' },
    { id: 2, name: 'Bob', role: 'user', status: 'active' },
    { id: 3, name: 'Charlie', role: 'guest', status: 'pending' }
  ]);
}

La nouvelle syntaxe offre de meilleures performances grâce à une compilation optimisée, une meilleure lisibilité, et des fonctionnalités comme @empty pour @for.

12. Comment configurer le Lazy Loading avec les standalone components ?

Le lazy loading charge les modules ou composants à la demande, réduisant le temps de chargement initial.

app.routes.tstypescript
import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: '',
    // Composant chargé immédiatement
    loadComponent: () => import('./home/home.component')
      .then(m => m.HomeComponent)
  },
  {
    path: 'products',
    // Lazy loading d'un composant standalone
    loadComponent: () => import('./products/product-list.component')
      .then(m => m.ProductListComponent)
  },
  {
    path: 'admin',
    // Lazy loading de routes enfants
    loadChildren: () => import('./admin/admin.routes')
      .then(m => m.adminRoutes),
    // Guard pour l'authentification
    canActivate: [authGuard]
  },
  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard/dashboard.component')
      .then(m => m.DashboardComponent),
    // Routes enfants inline
    children: [
      {
        path: 'stats',
        loadComponent: () => import('./dashboard/stats/stats.component')
          .then(m => m.StatsComponent)
      },
      {
        path: 'settings',
        loadComponent: () => import('./dashboard/settings/settings.component')
          .then(m => m.SettingsComponent)
      }
    ]
  }
];

// admin/admin.routes.ts
import { Routes } from '@angular/router';

export const adminRoutes: Routes = [
  {
    path: '',
    loadComponent: () => import('./admin-layout.component')
      .then(m => m.AdminLayoutComponent),
    children: [
      {
        path: 'users',
        loadComponent: () => import('./users/users.component')
          .then(m => m.UsersComponent)
      },
      {
        path: 'reports',
        loadComponent: () => import('./reports/reports.component')
          .then(m => m.ReportsComponent)
      }
    ]
  }
];

// main.ts - Bootstrap avec routes
import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { AppComponent } from './app/app.component';
import { routes } from './app/app.routes';

bootstrapApplication(AppComponent, {
  providers: [
    provideRouter(routes)
  ]
});

Avec les standalone components, le lazy loading est plus simple et plus granulaire qu'avec les NgModules.

Formulaires Angular

13. Quelle est la différence entre Template-driven et Reactive Forms ?

Angular propose deux approches pour gérer les formulaires, chacune adaptée à des cas d'usage différents.

typescript
// TEMPLATE-DRIVEN FORMS
// Simple, déclaratif, adapté aux formulaires basiques
// template-form.component.ts
import { Component } from '@angular/core';
import { FormsModule, NgForm } from '@angular/forms';

@Component({
  selector: 'app-template-form',
  standalone: true,
  imports: [FormsModule],
  template: `
    <form #loginForm="ngForm" (ngSubmit)="onSubmit(loginForm)">
      <input
        name="email"
        type="email"
        [(ngModel)]="user.email"
        required
        email
        #emailField="ngModel"
      >
      @if (emailField.invalid && emailField.touched) {
        <span class="error">Email invalide</span>
      }

      <input
        name="password"
        type="password"
        [(ngModel)]="user.password"
        required
        minlength="8"
        #passwordField="ngModel"
      >
      @if (passwordField.errors?.['minlength']) {
        <span class="error">Minimum 8 caractères</span>
      }

      <button type="submit" [disabled]="loginForm.invalid">
        Connexion
      </button>
    </form>
  `
})
export class TemplateFormComponent {
  user = { email: '', password: '' };

  onSubmit(form: NgForm) {
    if (form.valid) {
      console.log('Form data:', this.user);
    }
  }
}

// REACTIVE FORMS
// Plus de contrôle, testable, adapté aux formulaires complexes
// reactive-form.component.ts
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';

@Component({
  selector: 'app-reactive-form',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="loginForm" (ngSubmit)="onSubmit()">
      <input formControlName="email" type="email">
      @if (loginForm.get('email')?.hasError('required') && loginForm.get('email')?.touched) {
        <span class="error">Email requis</span>
      }
      @if (loginForm.get('email')?.hasError('email')) {
        <span class="error">Format email invalide</span>
      }

      <input formControlName="password" type="password">
      @if (loginForm.get('password')?.hasError('minlength')) {
        <span class="error">Minimum 8 caractères</span>
      }

      <button type="submit" [disabled]="loginForm.invalid">
        Connexion
      </button>
    </form>
  `
})
export class ReactiveFormComponent {
  private fb = inject(FormBuilder);

  // Définition programmatique du formulaire
  loginForm = this.fb.group({
    email: ['', [Validators.required, Validators.email]],
    password: ['', [Validators.required, Validators.minLength(8)]]
  });

  onSubmit() {
    if (this.loginForm.valid) {
      console.log('Form data:', this.loginForm.value);
    }
  }
}

Les Reactive Forms sont recommandés pour les formulaires complexes car ils offrent plus de flexibilité, sont plus faciles à tester, et permettent une meilleure réutilisation de la logique de validation.

14. Comment créer un validateur personnalisé ?

Les validateurs personnalisés permettent d'implémenter des règles de validation métier spécifiques.

validators/custom.validators.tstypescript
import { AbstractControl, ValidationErrors, ValidatorFn, AsyncValidatorFn } from '@angular/forms';
import { Observable, of, map, delay } from 'rxjs';

// Validateur synchrone - vérifie immédiatement
export function forbiddenNameValidator(forbiddenName: RegExp): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const forbidden = forbiddenName.test(control.value);
    return forbidden ? { forbiddenName: { value: control.value } } : null;
  };
}

// Validateur pour confirmer un mot de passe
export function passwordMatchValidator(): ValidatorFn {
  return (group: AbstractControl): ValidationErrors | null => {
    const password = group.get('password')?.value;
    const confirmPassword = group.get('confirmPassword')?.value;

    return password === confirmPassword ? null : { passwordMismatch: true };
  };
}

// Validateur asynchrone - vérifie via API
export function uniqueEmailValidator(userService: UserService): AsyncValidatorFn {
  return (control: AbstractControl): Observable<ValidationErrors | null> => {
    if (!control.value) {
      return of(null);
    }

    return userService.checkEmailExists(control.value).pipe(
      map(exists => exists ? { emailTaken: true } : null)
    );
  };
}

// Utilisation dans un composant
// registration.component.ts
import { Component, inject } from '@angular/core';
import { ReactiveFormsModule, FormBuilder, Validators } from '@angular/forms';
import { forbiddenNameValidator, passwordMatchValidator, uniqueEmailValidator } from './validators/custom.validators';
import { UserService } from './user.service';

@Component({
  selector: 'app-registration',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <form [formGroup]="registrationForm" (ngSubmit)="onSubmit()">
      <div>
        <input formControlName="username" placeholder="Nom d'utilisateur">
        @if (registrationForm.get('username')?.hasError('forbiddenName')) {
          <span class="error">Ce nom n'est pas autorisé</span>
        }
      </div>

      <div>
        <input formControlName="email" type="email" placeholder="Email">
        @if (registrationForm.get('email')?.pending) {
          <span class="info">Vérification en cours...</span>
        }
        @if (registrationForm.get('email')?.hasError('emailTaken')) {
          <span class="error">Cet email est déjà utilisé</span>
        }
      </div>

      <div formGroupName="passwords">
        <input formControlName="password" type="password" placeholder="Mot de passe">
        <input formControlName="confirmPassword" type="password" placeholder="Confirmer">
        @if (registrationForm.get('passwords')?.hasError('passwordMismatch')) {
          <span class="error">Les mots de passe ne correspondent pas</span>
        }
      </div>

      <button type="submit" [disabled]="registrationForm.invalid || registrationForm.pending">
        S'inscrire
      </button>
    </form>
  `
})
export class RegistrationComponent {
  private fb = inject(FormBuilder);
  private userService = inject(UserService);

  registrationForm = this.fb.group({
    username: ['', [
      Validators.required,
      forbiddenNameValidator(/admin/i)
    ]],
    email: ['',
      [Validators.required, Validators.email],
      [uniqueEmailValidator(this.userService)]
    ],
    passwords: this.fb.group({
      password: ['', [Validators.required, Validators.minLength(8)]],
      confirmPassword: ['', Validators.required]
    }, { validators: passwordMatchValidator() })
  });

  onSubmit() {
    if (this.registrationForm.valid) {
      console.log(this.registrationForm.value);
    }
  }
}

Les validateurs asynchrones sont particulièrement utiles pour les vérifications côté serveur comme l'unicité d'un email ou d'un nom d'utilisateur.

Routing et Navigation

15. Comment protéger les routes avec des Guards ?

Les guards contrôlent l'accès aux routes en fonction de conditions comme l'authentification ou les permissions.

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

// Guard fonctionnel (recommandé depuis Angular 15+)
export const authGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  if (authService.isAuthenticated()) {
    return true;
  }

  // Rediriger vers la page de login avec l'URL de retour
  return router.createUrlTree(['/login'], {
    queryParams: { returnUrl: state.url }
  });
};

// Guard pour les rôles
export const roleGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  const requiredRoles = route.data['roles'] as string[];
  const userRole = authService.getUserRole();

  if (requiredRoles.includes(userRole)) {
    return true;
  }

  return router.createUrlTree(['/unauthorized']);
};

// Guard pour le lazy loading (canMatch)
export const featureGuard: CanMatchFn = (route, segments) => {
  const featureService = inject(FeatureService);
  return featureService.isFeatureEnabled(route.path || '');
};

// Configuration des routes avec guards
// app.routes.ts
export const routes: Routes = [
  { path: 'login', loadComponent: () => import('./login.component') },

  {
    path: 'dashboard',
    loadComponent: () => import('./dashboard.component'),
    canActivate: [authGuard]
  },

  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.routes'),
    canActivate: [authGuard, roleGuard],
    canMatch: [featureGuard],
    data: { roles: ['admin', 'superadmin'] }
  },

  {
    path: 'settings',
    loadComponent: () => import('./settings.component'),
    canActivate: [authGuard],
    // Guard pour quitter la page (données non sauvegardées)
    canDeactivate: [unsavedChangesGuard]
  }
];

// Guard pour les modifications non sauvegardées
export const unsavedChangesGuard: CanDeactivateFn<{ hasUnsavedChanges: () => boolean }> =
  (component) => {
    if (component.hasUnsavedChanges()) {
      return confirm('Des modifications non sauvegardées seront perdues. Continuer ?');
    }
    return true;
  };

Les guards fonctionnels sont plus simples et s'intègrent mieux avec l'injection de dépendances moderne.

16. Comment passer des données entre routes ?

Angular offre plusieurs méthodes pour transmettre des données lors de la navigation.

1. Route Parameters (dans l'URL)typescript
// app.routes.ts
export const routes: Routes = [
  { path: 'products/:id', loadComponent: () => import('./product-detail.component') }
];

// product-detail.component.ts
import { Component, inject, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';

@Component({
  selector: 'app-product-detail',
  standalone: true,
  template: `<h1>Produit {{ productId }}</h1>`
})
export class ProductDetailComponent implements OnInit {
  private route = inject(ActivatedRoute);
  productId!: string;

  ngOnInit() {
    // Accès aux paramètres de route
    this.productId = this.route.snapshot.paramMap.get('id')!;

    // Ou de manière réactive
    this.route.paramMap.subscribe(params => {
      this.productId = params.get('id')!;
    });
  }
}

// 2. Query Parameters (?key=value)
// navigation.component.ts
import { Component, inject } from '@angular/core';
import { Router } from '@angular/router';

@Component({
  selector: 'app-navigation',
  standalone: true,
  template: `
    <button (click)="navigateWithQuery()">Rechercher</button>
  `
})
export class NavigationComponent {
  private router = inject(Router);

  navigateWithQuery() {
    this.router.navigate(['/products'], {
      queryParams: { category: 'electronics', sort: 'price' }
    });
  }
}

// 3. State (données non visibles dans l'URL)
// order-confirmation.component.ts
@Component({
  selector: 'app-order-confirmation',
  standalone: true,
  template: `
    @if (orderData) {
      <h1>Commande #{{ orderData.orderId }} confirmée</h1>
    }
  `
})
export class OrderConfirmationComponent implements OnInit {
  private route = inject(ActivatedRoute);
  orderData: any;

  ngOnInit() {
    // Récupérer le state passé via navigation
    this.orderData = history.state;
  }
}

// Naviguer avec state
this.router.navigate(['/order-confirmation'], {
  state: { orderId: '12345', total: 99.99 }
});

// 4. Resolvers (pré-chargement de données)
// product.resolver.ts
import { inject } from '@angular/core';
import { ResolveFn } from '@angular/router';
import { ProductService } from './product.service';

export const productResolver: ResolveFn<Product> = (route) => {
  const productService = inject(ProductService);
  const id = route.paramMap.get('id')!;
  return productService.getProduct(id);
};

// Route avec resolver
{
  path: 'products/:id',
  loadComponent: () => import('./product-detail.component'),
  resolve: { product: productResolver }
}

// Accès aux données résolues
ngOnInit() {
  this.product = this.route.snapshot.data['product'];
}
State et rafraîchissement

Les données passées via state sont perdues au rafraîchissement de la page. Pour les données persistantes, utiliser les paramètres d'URL ou stocker dans un service.

Performance et optimisation

17. Comment fonctionne la détection de changement dans Angular ?

Angular utilise Zone.js pour détecter les événements asynchrones et déclencher la vérification des composants.

change-detection.component.tstypescript
import {
  Component,
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  inject,
  signal
} from '@angular/core';

// Stratégie par défaut : vérifie tout l'arbre de composants
@Component({
  selector: 'app-default-strategy',
  template: `<p>{{ data }}</p>`
})
export class DefaultStrategyComponent {
  data = 'Hello';
}

// Stratégie OnPush : vérifie uniquement si les inputs changent
// ou si un événement est déclenché dans le composant
@Component({
  selector: 'app-onpush-strategy',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <p>{{ data().name }}</p>
    <button (click)="update()">Mettre à jour</button>
  `
})
export class OnPushStrategyComponent {
  private cdr = inject(ChangeDetectorRef);

  // Signal : déclenche automatiquement la détection
  data = signal({ name: 'Alice' });

  update() {
    // Avec signal, la mise à jour est automatique
    this.data.set({ name: 'Bob' });
  }

  // Pour les cas où la détection manuelle est nécessaire
  manualUpdate() {
    // Marquer le composant pour vérification
    this.cdr.markForCheck();

    // Ou forcer une détection immédiate
    this.cdr.detectChanges();
  }
}

// Exemple pratique : liste optimisée
@Component({
  selector: 'app-optimized-list',
  standalone: true,
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @for (item of items(); track item.id) {
      <!-- Chaque item a son propre composant OnPush -->
      <app-list-item [item]="item" />
    }
  `
})
export class OptimizedListComponent {
  items = signal<Item[]>([]);

  addItem(item: Item) {
    // Créer un nouveau tableau pour déclencher la détection
    this.items.update(current => [...current, item]);
  }

  updateItem(id: number, changes: Partial<Item>) {
    this.items.update(current =>
      current.map(item =>
        item.id === id ? { ...item, ...changes } : item
      )
    );
  }
}

La stratégie OnPush combinée aux Signals offre les meilleures performances en limitant les vérifications aux composants réellement modifiés.

18. Comment optimiser les performances d'une application Angular ?

Plusieurs techniques permettent d'améliorer les performances d'une application Angular.

1. Lazy Loading des routes (voir question 12)typescript
// 2. TrackBy pour les listes (obligatoire avec @for)
@Component({
  template: `
    @for (user of users(); track user.id) {
      <app-user-card [user]="user" />
    }
  `
})
export class UserListComponent {
  users = signal<User[]>([]);
}

// 3. Pipes purs pour les transformations
// format-date.pipe.ts
import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'formatDate',
  standalone: true,
  pure: true // Par défaut, recalculé uniquement si l'input change
})
export class FormatDatePipe implements PipeTransform {
  transform(value: Date, format: string = 'short'): string {
    return new Intl.DateTimeFormat('fr-FR', {
      dateStyle: format as any
    }).format(value);
  }
}

// 4. Virtual Scrolling pour les grandes listes
import { Component } from '@angular/core';
import { ScrollingModule } from '@angular/cdk/scrolling';

@Component({
  selector: 'app-virtual-list',
  standalone: true,
  imports: [ScrollingModule],
  template: `
    <!-- Seuls les éléments visibles sont rendus -->
    <cdk-virtual-scroll-viewport itemSize="50" class="viewport">
      <div *cdkVirtualFor="let item of items" class="item">
        {{ item.name }}
      </div>
    </cdk-virtual-scroll-viewport>
  `,
  styles: [`
    .viewport { height: 400px; }
    .item { height: 50px; }
  `]
})
export class VirtualListComponent {
  items = Array.from({ length: 10000 }, (_, i) => ({
    id: i,
    name: `Item ${i}`
  }));
}

// 5. Defer pour le chargement différé (Angular 17+)
@Component({
  template: `
    <header>Toujours chargé immédiatement</header>

    <!-- Chargé quand visible dans le viewport -->
    @defer (on viewport) {
      <app-heavy-component />
    } @placeholder {
      <div class="skeleton">Chargement...</div>
    } @loading (minimum 500ms) {
      <app-spinner />
    }

    <!-- Chargé après interaction -->
    @defer (on interaction) {
      <app-comments />
    } @placeholder {
      <button>Afficher les commentaires</button>
    }

    <!-- Chargé après un délai -->
    @defer (on timer(2000ms)) {
      <app-analytics />
    }
  `
})
export class OptimizedPageComponent {}

La combinaison de ces techniques avec le profiling via Angular DevTools permet d'identifier et résoudre les goulots d'étranglement.

Prêt à réussir tes entretiens Angular ?

Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.

Communication entre composants

19. Quelles sont les différentes méthodes de communication entre composants ?

Angular offre plusieurs patterns pour la communication entre composants selon leur relation hiérarchique.

1. Parent → Enfant : @Input / input()typescript
// parent.component.ts
@Component({
  template: `<app-child [message]="parentMessage" />`
})
export class ParentComponent {
  parentMessage = 'Hello from parent';
}

// child.component.ts
@Component({
  template: `<p>{{ message() }}</p>`
})
export class ChildComponent {
  message = input.required<string>();
}

// 2. Enfant → Parent : @Output / output()
// child.component.ts
@Component({
  template: `<button (click)="sendMessage()">Envoyer</button>`
})
export class ChildComponent {
  messageEvent = output<string>();

  sendMessage() {
    this.messageEvent.emit('Hello from child');
  }
}

// parent.component.ts
@Component({
  template: `<app-child (messageEvent)="onMessage($event)" />`
})
export class ParentComponent {
  onMessage(message: string) {
    console.log(message);
  }
}

// 3. Via Service partagé (composants non liés)
// message.service.ts
@Injectable({ providedIn: 'root' })
export class MessageService {
  private messageSubject = new Subject<string>();
  message$ = this.messageSubject.asObservable();

  // Ou avec Signal
  currentMessage = signal<string>('');

  sendMessage(message: string) {
    this.messageSubject.next(message);
    this.currentMessage.set(message);
  }
}

// component-a.ts
@Component({
  template: `<button (click)="send()">Envoyer</button>`
})
export class ComponentA {
  private messageService = inject(MessageService);

  send() {
    this.messageService.sendMessage('Hello from A');
  }
}

// component-b.ts
@Component({
  template: `<p>{{ message$ | async }}</p>`
})
export class ComponentB implements OnDestroy {
  private messageService = inject(MessageService);
  private destroy$ = new Subject<void>();

  message$ = this.messageService.message$;

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

// 4. ViewChild pour accéder à un enfant
@Component({
  template: `<app-timer #timer />`
})
export class ParentComponent implements AfterViewInit {
  @ViewChild('timer') timerComponent!: TimerComponent;

  ngAfterViewInit() {
    this.timerComponent.start();
  }
}

// 5. ContentChild pour le contenu projeté
@Component({
  selector: 'app-card',
  template: `
    <div class="card">
      <ng-content select="[card-header]" />
      <ng-content />
    </div>
  `
})
export class CardComponent implements AfterContentInit {
  @ContentChild('header') header!: ElementRef;

  ngAfterContentInit() {
    console.log('Header content:', this.header);
  }
}

Le choix de la méthode dépend de la relation entre les composants et de la complexité de la communication.

20. Comment implémenter un système de gestion d'état avec Signals ?

Les Signals permettent de créer un store réactif simple sans bibliothèque externe.

store/cart.store.tstypescript
import { Injectable, signal, computed } from '@angular/core';

interface CartItem {
  id: number;
  name: string;
  price: number;
  quantity: number;
}

interface CartState {
  items: CartItem[];
  loading: boolean;
  error: string | null;
}

@Injectable({ providedIn: 'root' })
export class CartStore {
  // État privé
  private state = signal<CartState>({
    items: [],
    loading: false,
    error: null
  });

  // Sélecteurs publics (lecture seule)
  readonly items = computed(() => this.state().items);
  readonly loading = computed(() => this.state().loading);
  readonly error = computed(() => this.state().error);

  readonly itemCount = computed(() =>
    this.state().items.reduce((sum, item) => sum + item.quantity, 0)
  );

  readonly total = computed(() =>
    this.state().items.reduce(
      (sum, item) => sum + item.price * item.quantity,
      0
    )
  );

  readonly isEmpty = computed(() => this.state().items.length === 0);

  // Actions
  addItem(product: Omit<CartItem, 'quantity'>) {
    this.state.update(state => {
      const existingItem = state.items.find(i => i.id === product.id);

      if (existingItem) {
        return {
          ...state,
          items: state.items.map(item =>
            item.id === product.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          )
        };
      }

      return {
        ...state,
        items: [...state.items, { ...product, quantity: 1 }]
      };
    });
  }

  removeItem(id: number) {
    this.state.update(state => ({
      ...state,
      items: state.items.filter(item => item.id !== id)
    }));
  }

  updateQuantity(id: number, quantity: number) {
    if (quantity <= 0) {
      this.removeItem(id);
      return;
    }

    this.state.update(state => ({
      ...state,
      items: state.items.map(item =>
        item.id === id ? { ...item, quantity } : item
      )
    }));
  }

  clearCart() {
    this.state.update(state => ({ ...state, items: [] }));
  }

  // Action asynchrone
  async checkout() {
    this.state.update(s => ({ ...s, loading: true, error: null }));

    try {
      // Appel API
      await fetch('/api/checkout', {
        method: 'POST',
        body: JSON.stringify({ items: this.items() })
      });

      this.clearCart();
    } catch (e) {
      this.state.update(s => ({
        ...s,
        error: 'Erreur lors de la commande'
      }));
    } finally {
      this.state.update(s => ({ ...s, loading: false }));
    }
  }
}

// Utilisation dans un composant
// cart.component.ts
@Component({
  selector: 'app-cart',
  standalone: true,
  template: `
    @if (cartStore.isEmpty()) {
      <p>Votre panier est vide</p>
    } @else {
      @for (item of cartStore.items(); track item.id) {
        <div class="cart-item">
          <span>{{ item.name }}</span>
          <span>{{ item.price }} € × {{ item.quantity }}</span>
          <button (click)="cartStore.removeItem(item.id)">Supprimer</button>
        </div>
      }

      <div class="cart-total">
        <strong>Total: {{ cartStore.total() }} €</strong>
        <span>({{ cartStore.itemCount() }} articles)</span>
      </div>

      <button
        (click)="cartStore.checkout()"
        [disabled]="cartStore.loading()"
      >
        @if (cartStore.loading()) {
          Traitement...
        } @else {
          Commander
        }
      </button>
    }
  `
})
export class CartComponent {
  cartStore = inject(CartStore);
}

Ce pattern offre une gestion d'état prévisible et réactive sans la complexité de NgRx pour les applications de taille moyenne.

Testing Angular

21. Comment tester un composant Angular ?

Les tests de composants vérifient le rendu, les interactions et l'intégration avec les services.

user-card.component.spec.tstypescript
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { UserCardComponent } from './user-card.component';

describe('UserCardComponent', () => {
  let component: UserCardComponent;
  let fixture: ComponentFixture<UserCardComponent>;

  beforeEach(async () => {
    await TestBed.configureTestingModule({
      imports: [UserCardComponent]
    }).compileComponents();

    fixture = TestBed.createComponent(UserCardComponent);
    component = fixture.componentInstance;
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should display user name', () => {
    // Arrange
    fixture.componentRef.setInput('user', {
      name: 'Alice',
      email: 'alice@test.com'
    });

    // Act
    fixture.detectChanges();

    // Assert
    const nameElement = fixture.debugElement.query(By.css('.user-name'));
    expect(nameElement.nativeElement.textContent).toContain('Alice');
  });

  it('should emit event when delete button clicked', () => {
    // Arrange
    fixture.componentRef.setInput('user', { id: 1, name: 'Alice' });
    fixture.detectChanges();

    const deleteSpy = jest.spyOn(component.delete, 'emit');

    // Act
    const deleteButton = fixture.debugElement.query(By.css('.delete-btn'));
    deleteButton.triggerEventHandler('click', null);

    // Assert
    expect(deleteSpy).toHaveBeenCalledWith(1);
  });

  it('should show loading state', () => {
    // Arrange
    fixture.componentRef.setInput('isLoading', true);

    // Act
    fixture.detectChanges();

    // Assert
    const spinner = fixture.debugElement.query(By.css('.spinner'));
    expect(spinner).toBeTruthy();
  });
});

// Test avec service mocké
// user-list.component.spec.ts
import { of, throwError } from 'rxjs';

describe('UserListComponent', () => {
  let component: UserListComponent;
  let fixture: ComponentFixture<UserListComponent>;
  let mockUserService: jest.Mocked<UserService>;

  beforeEach(async () => {
    mockUserService = {
      getUsers: jest.fn(),
      deleteUser: jest.fn()
    } as any;

    await TestBed.configureTestingModule({
      imports: [UserListComponent],
      providers: [
        { provide: UserService, useValue: mockUserService }
      ]
    }).compileComponents();

    fixture = TestBed.createComponent(UserListComponent);
    component = fixture.componentInstance;
  });

  it('should load users on init', () => {
    // Arrange
    const users = [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }];
    mockUserService.getUsers.mockReturnValue(of(users));

    // Act
    fixture.detectChanges();

    // Assert
    expect(mockUserService.getUsers).toHaveBeenCalled();
    expect(component.users()).toEqual(users);
  });

  it('should handle error state', () => {
    // Arrange
    mockUserService.getUsers.mockReturnValue(
      throwError(() => new Error('Network error'))
    );

    // Act
    fixture.detectChanges();

    // Assert
    expect(component.error()).toBe('Erreur de chargement');
  });
});

22. Comment tester un service Angular ?

Les tests de services vérifient la logique métier et les interactions avec les APIs.

auth.service.spec.tstypescript
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { AuthService } from './auth.service';

describe('AuthService', () => {
  let service: AuthService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [AuthService]
    });

    service = TestBed.inject(AuthService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => {
    // Vérifier qu'il n'y a pas de requêtes en attente
    httpMock.verify();
  });

  describe('login', () => {
    it('should return user on successful login', () => {
      // Arrange
      const credentials = { email: 'test@test.com', password: 'password' };
      const mockResponse = { id: 1, email: 'test@test.com', token: 'abc123' };

      // Act
      service.login(credentials).subscribe(user => {
        // Assert
        expect(user).toEqual(mockResponse);
        expect(service.isAuthenticated()).toBe(true);
      });

      // Simuler la réponse HTTP
      const req = httpMock.expectOne('/api/auth/login');
      expect(req.request.method).toBe('POST');
      expect(req.request.body).toEqual(credentials);
      req.flush(mockResponse);
    });

    it('should handle login error', () => {
      // Arrange
      const credentials = { email: 'test@test.com', password: 'wrong' };

      // Act
      service.login(credentials).subscribe({
        error: (error) => {
          // Assert
          expect(error.status).toBe(401);
          expect(service.isAuthenticated()).toBe(false);
        }
      });

      // Simuler une erreur
      const req = httpMock.expectOne('/api/auth/login');
      req.flush({ message: 'Invalid credentials' }, { status: 401, statusText: 'Unauthorized' });
    });
  });

  describe('logout', () => {
    it('should clear authentication state', () => {
      // Arrange - simuler un utilisateur connecté
      service['currentUser'].set({ id: 1, email: 'test@test.com' });

      // Act
      service.logout();

      // Assert
      expect(service.isAuthenticated()).toBe(false);
      expect(service.getCurrentUser()).toBeNull();
    });
  });
});

Questions avancées

23. Comment fonctionne le Server-Side Rendering (SSR) avec Angular ?

Angular Universal permet le rendu côté serveur pour améliorer le SEO et les performances perçues.

typescript
// Configuration SSR avec Angular 17+
// app.config.server.ts
import { ApplicationConfig, mergeApplicationConfig } from '@angular/core';
import { provideServerRendering } from '@angular/platform-server';
import { appConfig } from './app.config';

const serverConfig: ApplicationConfig = {
  providers: [
    provideServerRendering()
  ]
};

export const config = mergeApplicationConfig(appConfig, serverConfig);

// Gestion des APIs browser uniquement
// platform.service.ts
import { Injectable, PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';

@Injectable({ providedIn: 'root' })
export class PlatformService {
  private platformId = inject(PLATFORM_ID);

  get isBrowser(): boolean {
    return isPlatformBrowser(this.platformId);
  }

  get isServer(): boolean {
    return isPlatformServer(this.platformId);
  }
}

// Composant SSR-aware
// analytics.component.ts
@Component({
  selector: 'app-analytics',
  standalone: true,
  template: `
    @if (platform.isBrowser) {
      <div id="analytics-container"></div>
    }
  `
})
export class AnalyticsComponent implements OnInit {
  platform = inject(PlatformService);

  ngOnInit() {
    // Code exécuté uniquement côté client
    if (this.platform.isBrowser) {
      this.initializeAnalytics();
    }
  }

  private initializeAnalytics() {
    // window et document sont disponibles
    console.log('Analytics initialized on', window.location.href);
  }
}

// TransferState pour éviter les doubles requêtes
// product.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { TransferState, makeStateKey } from '@angular/core';
import { of, tap } from 'rxjs';

const PRODUCTS_KEY = makeStateKey<Product[]>('products');

@Injectable({ providedIn: 'root' })
export class ProductService {
  private http = inject(HttpClient);
  private transferState = inject(TransferState);
  private platform = inject(PlatformService);

  getProducts() {
    // Côté client : vérifier si les données existent déjà
    if (this.platform.isBrowser) {
      const cachedProducts = this.transferState.get(PRODUCTS_KEY, null);
      if (cachedProducts) {
        this.transferState.remove(PRODUCTS_KEY);
        return of(cachedProducts);
      }
    }

    return this.http.get<Product[]>('/api/products').pipe(
      tap(products => {
        // Côté serveur : stocker pour le client
        if (this.platform.isServer) {
          this.transferState.set(PRODUCTS_KEY, products);
        }
      })
    );
  }
}

Le SSR améliore le First Contentful Paint et permet aux moteurs de recherche d'indexer le contenu dynamique.

24. Comment gérer l'internationalisation (i18n) dans Angular ?

Angular propose plusieurs approches pour l'internationalisation des applications.

1. Angular i18n intégré (compilation séparée par langue)typescript
// app.component.ts
@Component({
  template: `
    <h1 i18n="page title|Main heading@@homeTitle">
      Bienvenue sur notre application
    </h1>

    <p i18n="@@itemCount">
      {itemCount, plural,
        =0 {Aucun article}
        =1 {Un article}
        other {{{itemCount}} articles}
      }
    </p>

    <button i18n-title="@@addToCartTitle" title="Ajouter au panier">
      <span i18n="@@addToCart">Ajouter</span>
    </button>
  `
})
export class AppComponent {
  itemCount = 5;
}

// 2. ngx-translate (changement de langue à runtime)
// app.config.ts
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';

export function HttpLoaderFactory(http: HttpClient) {
  return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}

// Configuration
provideTranslateService({
  defaultLanguage: 'fr',
  loader: {
    provide: TranslateLoader,
    useFactory: HttpLoaderFactory,
    deps: [HttpClient]
  }
})

// language-switcher.component.ts
@Component({
  selector: 'app-language-switcher',
  standalone: true,
  imports: [TranslateModule],
  template: `
    <select (change)="changeLanguage($event)">
      <option value="fr">Français</option>
      <option value="en">English</option>
      <option value="es">Español</option>
    </select>

    <!-- Utilisation dans le template -->
    <h1>{{ 'HOME.TITLE' | translate }}</h1>
    <p>{{ 'HOME.WELCOME' | translate:{ name: userName } }}</p>
  `
})
export class LanguageSwitcherComponent {
  private translate = inject(TranslateService);
  userName = 'Alice';

  changeLanguage(event: Event) {
    const lang = (event.target as HTMLSelectElement).value;
    this.translate.use(lang);
  }
}

// assets/i18n/fr.json
{
  "HOME": {
    "TITLE": "Bienvenue",
    "WELCOME": "Bonjour {{name}} !"
  }
}

// assets/i18n/en.json
{
  "HOME": {
    "TITLE": "Welcome",
    "WELCOME": "Hello {{name}}!"
  }
}

Le choix entre i18n intégré et ngx-translate dépend des besoins : compilation séparée pour les meilleures performances, ou changement dynamique pour plus de flexibilité.

25. Quelles sont les bonnes pratiques de structuration d'un projet Angular ?

Une structure bien organisée facilite la maintenance et la scalabilité du projet.

text
src/
├── app/
│   ├── core/                    # Services singleton, guards, interceptors
│   │   ├── guards/
│   │   │   └── auth.guard.ts
│   │   ├── interceptors/
│   │   │   └── auth.interceptor.ts
│   │   ├── services/
│   │   │   ├── auth.service.ts
│   │   │   └── api.service.ts
│   │   └── core.provider.ts     # Configuration des providers
│   │
│   ├── shared/                  # Composants, pipes, directives réutilisables
│   │   ├── components/
│   │   │   ├── button/
│   │   │   └── modal/
│   │   ├── directives/
│   │   ├── pipes/
│   │   └── index.ts             # Barrel exports
│   │
│   ├── features/                # Modules fonctionnels (lazy-loaded)
│   │   ├── products/
│   │   │   ├── components/
│   │   │   ├── services/
│   │   │   ├── models/
│   │   │   ├── products.routes.ts
│   │   │   └── products.component.ts
│   │   ├── cart/
│   │   └── checkout/
│   │
│   ├── layouts/                 # Layouts de page
│   │   ├── main-layout/
│   │   └── auth-layout/
│   │
│   ├── app.component.ts
│   ├── app.config.ts
│   └── app.routes.ts
├── assets/
├── environments/
└── styles/
typescript
// Bonnes pratiques de code
// 1. Barrel exports pour simplifier les imports
// shared/index.ts
export * from './components/button/button.component';
export * from './components/modal/modal.component';
export * from './pipes/format-date.pipe';

// 2. Configuration centralisée des providers
// core/core.provider.ts
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './interceptors/auth.interceptor';

export const coreProviders = [
  provideHttpClient(
    withInterceptors([authInterceptor])
  ),
  // Autres providers globaux
];

// 3. Modèles typés
// features/products/models/product.model.ts
export interface Product {
  id: number;
  name: string;
  price: number;
  category: ProductCategory;
  createdAt: Date;
}

export type ProductCategory = 'electronics' | 'clothing' | 'books';

export interface CreateProductDto {
  name: string;
  price: number;
  category: ProductCategory;
}

// 4. Interceptor fonctionnel
// core/interceptors/auth.interceptor.ts
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';

export const authInterceptor: HttpInterceptorFn = (req, next) => {
  const authService = inject(AuthService);
  const token = authService.getToken();

  if (token) {
    req = req.clone({
      setHeaders: { Authorization: `Bearer ${token}` }
    });
  }

  return next(req);
};

Ces conventions permettent une navigation intuitive dans le code et facilitent le travail en équipe.

Conclusion

Ces 25 questions couvrent les concepts essentiels d'Angular demandés en entretien. Les points clés à maîtriser :

  • Fondamentaux : Composants, data binding, cycle de vie, DI
  • Angular moderne : Signals, standalone components, nouveau control flow
  • Réactivité : RxJS, Observables, gestion d'état avec Signals
  • Formulaires : Template-driven vs Reactive, validation personnalisée
  • Routing : Guards, lazy loading, passage de données
  • Performance : OnPush, defer, virtual scrolling
  • Testing : Tests unitaires, mocks, HttpTestingController

La préparation aux entretiens Angular nécessite une pratique régulière. Construire des projets personnels permet de consolider ces connaissances et de pouvoir les expliquer naturellement lors de l'entretien.

Passe à la pratique !

Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.

Tags

#angular interview
#frontend interview
#angular questions
#typescript
#entretien technique

Partager

Articles similaires