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.

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.
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é.
// 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.
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.
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.
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.
// 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.
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.
// 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.
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.
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.
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().
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.
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.
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.
// 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.
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.
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.
// 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'];
}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.
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.
// 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.
// 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.
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.
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.
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.
// 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.
// 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.
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/// 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
Partager
Articles similaires

Angular 19 Zoneless : Detection de Changements sans Zone.js et Gains de Performance
Guide complet sur la detection de changements sans Zone.js dans Angular 19 et 20. Configuration de provideZonelessChangeDetection, migration vers les signals, SSR sans Zone.js, benchmarks de performance et pieges a eviter lors de la migration.

Angular Standalone Components : Migration et Bonnes Pratiques en 2026
Guide complet pour migrer les applications Angular des NgModules vers les standalone components. Processus de migration CLI en 3 etapes, lazy loading, routage, tests et bonnes pratiques pour Angular 21.

Angular 19 en entretien : Signals, SSR et les questions incontournables
Les questions d'entretien Angular 19 les plus fréquentes : Signals, SSR avec hydratation incrémentale, détection de changement zoneless et nouvelles API réactives.