En bref
Quand les règles de validation d’un formulaire Angular changent selon le rôle, le produit ou le pays, les if/else dans le composant deviennent ingérables. Le pattern Strategy isole chaque jeu de règles dans une classe dédiée, un resolver choisit la bonne stratégie, et une factory l’applique au formulaire — y compris à chaud, sans recréer le formulaire. Résultat : un composant qui ne connaît plus aucune règle métier, et des règles testables en trois lignes.
Pattern Strategy
Design pattern (Gang of Four) qui encapsule des règles interchangeables derrière une interface commune, pour les choisir à l’exécution sans if/else.
FormGroup
Classe des Reactive Forms Angular qui regroupe plusieurs contrôles de formulaire et calcule leur validité globale.
FormRecord
Variante typée de FormGroup (Angular 14+) dont les clés sont dynamiques : idéale pour ajouter ou retirer des champs à l’exécution.
ValidatorFn
Signature d’une fonction de validation Angular : elle reçoit un contrôle et renvoie null si la valeur est valide, sinon un objet d’erreurs.
Validator custom
ValidatorFn écrit pour une règle métier précise (email d’entreprise, SIRET…), réutilisable dans plusieurs stratégies.
InjectionToken
Clé d’injection Angular pour fournir une valeur qui n’est pas une classe, comme une liste de stratégies.
Multi-provider
Option multi: true de l’injection de dépendances : plusieurs providers alimentent un même token, injecté sous forme de tableau.
Resolver de stratégie
Service qui choisit la bonne stratégie selon le contexte (rôle, pays, feature flag), à la place d’une cascade de conditions.
Factory
Objet chargé de construire et de réappliquer le formulaire à partir d’une stratégie, pour que le composant n’ait pas à le faire.
input() et effect()
API à base de signals (Angular 17+) : input() expose une propriété réactive, effect() réexécute du code à chaque changement de signal.
Signal Forms
Nouvelle API de formulaires à base de signals, introduite en expérimental dans Angular 21.
Principe Open/Closed (OCP)
Principe SOLID : un module est ouvert à l’extension mais fermé à la modification. Ajouter un profil = ajouter une classe.
Principe SRP
Single Responsibility Principle : une classe ne doit avoir qu’une seule raison de changer.
Introduction
Dans une application Angular riche en formulaires, la validation finit toujours par dépendre du contexte : un administrateur doit saisir des permissions, un partenaire un SIRET, un client particulier ni l’un ni l’autre. Un simple Validators.required ne suffit plus.
La tentation est de gérer ces variantes directement dans le composant. Ça fonctionne au début, puis chaque nouveau profil ajoute une branche, chaque branche un risque de régression. Le pattern Strategy (Gang of Four) règle ce problème proprement : on encapsule chaque jeu de règles dans une stratégie interchangeable, et le formulaire se contente de l’appliquer.
Ce guide montre une implémentation moderne (Angular 17+ : composants standalone, inject(), input() signal, contrôle de flux @if, formulaires typés avec FormRecord).
Problématique observée
- Les validations sont codées en dur dans les composants, mêlées à la logique d’affichage.
- Chaque nouveau cas d’usage ajoute des conditions imbriquées (
if/else,switch) qui nuisent à la lisibilité. - Une règle métier qui change (ex. « l’email admin doit être un email d’entreprise ») oblige à modifier plusieurs composants.
- Les tests unitaires doivent monter tout le composant pour vérifier une simple règle.
Tentatives ratées (et pourquoi)
- Un service unique qui centralise toutes les règles : il grossit à chaque profil et viole le principe de responsabilité unique (SRP). On a déplacé le
if/else, pas supprimé le problème. - Uniquement des validators custom : parfaits pour une règle ponctuelle (
corporateEmail,siret), mais ils ne décident pas quel ensemble de règles appliquer selon le contexte. - Des directives de validation : bonnes pour la réutilisation dans le template, mais le choix contextuel reste dispersé dans le HTML.
La bonne réponse combine les deux niveaux : des validators atomiques réutilisables, et des stratégies qui les assemblent par contexte.
Solution technique : le pattern Strategy
Le pattern repose sur quatre pièces : une interface commune, des stratégies concrètes, un resolver qui choisit la stratégie selon le contexte, et une factory qui l’applique au formulaire.
Strategy fait partie des patterns comportementaux du Gang of Four. Pour situer les trois familles de patterns et savoir quand les utiliser, lisez Design patterns : la base de tout développement logiciel robuste.
Étape 1 — L’interface et le token d’injection
On type les validators avec ValidatorFn (et non any[]), et on prévoit des validators de groupe pour les règles transverses (ex. confirmation d’email).
// validation-strategy.ts
import { InjectionToken } from '@angular/core';
import { ValidatorFn } from '@angular/forms';
export type UserRole = 'default' | 'admin' | 'partner';
export interface ValidationStrategy {
readonly role: UserRole;
controls(): Record<string, ValidatorFn[]>;
groupValidators?(): ValidatorFn[];
}
export const VALIDATION_STRATEGIES =
new InjectionToken<ValidationStrategy[]>('VALIDATION_STRATEGIES');
Étape 2 — Des validators atomiques réutilisables
// validators.ts
import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
export function corporateEmail(domain: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const value = (control.value ?? '') as string;
return !value || value.endsWith('@' + domain)
? null
: { corporateEmail: { domain } };
};
}
Étape 3 — Les stratégies concrètes
Chaque stratégie assemble les validators pour un contexte. Elle ne connaît ni le composant ni le template.
// strategies.ts
import { Validators } from '@angular/forms';
import { ValidationStrategy } from './validation-strategy';
import { corporateEmail } from './validators';
const base = {
username: [Validators.required, Validators.minLength(3)],
email: [Validators.required, Validators.email],
};
export class DefaultUserStrategy implements ValidationStrategy {
readonly role = 'default' as const;
controls() { return { ...base }; }
}
export class AdminUserStrategy implements ValidationStrategy {
readonly role = 'admin' as const;
controls() {
return {
...base,
email: [...base.email, corporateEmail('cleancodecraft.com')],
permissions: [Validators.required],
};
}
}
export class PartnerStrategy implements ValidationStrategy {
readonly role = 'partner' as const;
controls() {
return {
...base,
siret: [Validators.required, Validators.pattern(/^\d{14}$/)],
};
}
}
Étape 4 — Le resolver : choisir la stratégie sans if/else
Les stratégies sont enregistrées en multi-provider. Ajouter un profil revient à ajouter une ligne de configuration : le resolver, la factory et le composant restent inchangés (principe Open/Closed).
// strategy-resolver.ts
import { Injectable, inject } from '@angular/core';
import { UserRole, VALIDATION_STRATEGIES, ValidationStrategy } from './validation-strategy';
@Injectable({ providedIn: 'root' })
export class StrategyResolver {
private readonly strategies = inject(VALIDATION_STRATEGIES);
resolve(role: UserRole): ValidationStrategy {
const strategy = this.strategies.find(s => s.role === role);
if (!strategy) throw new Error(`Aucune stratégie pour le rôle "${role}"`);
return strategy;
}
}
// app.config.ts
export const appConfig: ApplicationConfig = {
providers: [
{ provide: VALIDATION_STRATEGIES, useClass: DefaultUserStrategy, multi: true },
{ provide: VALIDATION_STRATEGIES, useClass: AdminUserStrategy, multi: true },
{ provide: VALIDATION_STRATEGIES, useClass: PartnerStrategy, multi: true },
],
};
Étape 5 — La factory : créer et réappliquer à chaud
FormRecord (Angular 14+) est fait pour les formulaires aux clés dynamiques. La méthode apply() ajoute les champs manquants, retire ceux qui ne s’appliquent plus et met à jour les validators sans perdre les valeurs déjà saisies.
// dynamic-form.factory.ts
import { Injectable, inject } from '@angular/core';
import { FormControl, FormRecord, NonNullableFormBuilder } from '@angular/forms';
import { ValidationStrategy } from './validation-strategy';
@Injectable({ providedIn: 'root' })
export class DynamicFormFactory {
private readonly fb = inject(NonNullableFormBuilder);
create(strategy: ValidationStrategy): FormRecord<FormControl<string>> {
const form = this.fb.record<string>({});
this.apply(form, strategy);
return form;
}
apply(form: FormRecord<FormControl<string>>, strategy: ValidationStrategy): void {
const rules = strategy.controls();
Object.keys(form.controls)
.filter(name => !(name in rules))
.forEach(name => form.removeControl(name, { emitEvent: false }));
for (const [name, validators] of Object.entries(rules)) {
const control = form.controls[name] ?? this.fb.control('');
control.setValidators(validators);
if (!form.contains(name)) form.addControl(name, control, { emitEvent: false });
}
form.setValidators(strategy.groupValidators?.() ?? null);
Object.values(form.controls).forEach(c => c.updateValueAndValidity({ emitEvent: false }));
form.updateValueAndValidity();
}
}
Étape 6 — Le composant ne connaît plus aucune règle
Le rôle arrive en input() signal ; un effect() réapplique la stratégie à chaque changement. Le template utilise le contrôle de flux @if (Angular 17+).
// user-form.component.ts
import { Component, effect, inject, input } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
@Component({
selector: 'app-user-form',
imports: [ReactiveFormsModule],
template: `
<form [formGroup]="form" (ngSubmit)="submit()">
<input formControlName="username" placeholder="Nom d'utilisateur" />
<input formControlName="email" type="email" placeholder="Email" />
@if (form.contains('permissions')) {
<input formControlName="permissions" placeholder="Permissions" />
}
@if (form.contains('siret')) {
<input formControlName="siret" placeholder="SIRET (14 chiffres)" />
}
<button type="submit" [disabled]="form.invalid">Enregistrer</button>
</form>
`,
})
export class UserFormComponent {
private readonly factory = inject(DynamicFormFactory);
private readonly resolver = inject(StrategyResolver);
readonly role = input<UserRole>('default');
readonly form = this.factory.create(this.resolver.resolve('default'));
constructor() {
effect(() => this.factory.apply(this.form, this.resolver.resolve(this.role())));
}
submit(): void {
if (this.form.valid) console.log(this.form.getRawValue());
}
}
Note : depuis Angular 19, les composants sont standalone par défaut. Sur une version antérieure, ajoutez standalone: true.
Tester une stratégie en trois lignes
Chaque stratégie est une classe pure : pas de TestBed, pas de composant à monter. Ces tests fonctionnent avec Jasmine, Jest ou Vitest.
// admin-user.strategy.spec.ts
import { FormControl } from '@angular/forms';
import { AdminUserStrategy } from './strategies';
describe('AdminUserStrategy', () => {
const rules = new AdminUserStrategy().controls();
it('rend les permissions obligatoires', () => {
const control = new FormControl('', rules['permissions']);
expect(control.hasError('required')).toBe(true);
});
it('refuse un email hors du domaine de l\'entreprise', () => {
const control = new FormControl('jane@gmail.com', rules['email']);
expect(control.hasError('corporateEmail')).toBe(true);
});
});
Gains concrets
| Critère | Avant (if/else) | Après (Strategy) |
|---|---|---|
| Ajouter un profil | Modifier le composant et ses tests | Ajouter une classe + une ligne de provider |
| Lieu des règles métier | Dispersé dans les composants | Une classe par contexte |
| Tester une règle | Monter le composant avec TestBed | Instancier la stratégie |
| Changer de rôle à chaud | Recréer le formulaire | apply() conserve les valeurs saisies |
| Principes SOLID | SRP et OCP violés | SRP, OCP et DIP respectés |
Côté performance, l’impact est neutre : le gain se situe dans la maintenabilité et la réduction du risque de régression, pas dans le temps d’exécution.
Les principes Open/Closed et d’inversion des dépendances sont détaillés, exemple avant/après à l’appui, dans SOLID en JavaScript : les 5 principes expliqués avec du code.
Bonnes pratiques à appliquer ailleurs
- Strategy + Factory : le resolver choisit la stratégie à partir du contexte (rôle, feature flag, pays, type de produit).
- Rendu dynamique : le même pattern s’applique aux colonnes d’un tableau Angular Material ou aux étapes d’un stepper.
- Pilotage par l’état : avec NgRx ou un store à base de signals, le rôle provient d’un sélecteur et l’
effect()fait le reste. - Règles transverses : utilisez
groupValidators()pour les validations multi-champs (dates de début/fin, confirmation d’email). - Gardez les stratégies sans état : elles décrivent des règles, elles ne stockent pas de données.
Et les Signal Forms ?
Angular 21 a introduit les Signal Forms en mode expérimental. Le principe reste identique : vos stratégies peuvent produire un schéma de validation au lieu d’une liste de ValidatorFn. Pour une application en production, les Reactive Forms restent la valeur sûre tant que l’API n’est pas stabilisée.
Pour que ces formulaires dynamiques restent rapides, combinez-les avec la stratégie OnPush et des composants qui ne dépendent que de leurs inputs : voir Angular performance et clean code.
Références
- Angular — Validating form input (documentation officielle)
- Angular — Typed forms et FormRecord
- Gamma, Helm, Johnson, Vlissides — Design Patterns: Elements of Reusable Object-Oriented Software (pattern Strategy)
- Robert C. Martin — Clean Architecture (principes SRP, OCP, DIP)
- Refactoring.Guru — Strategy
FAQ
Pourquoi ne pas utiliser uniquement des validators custom ?
Un validator custom répond à une règle ponctuelle (un email d’entreprise, un SIRET). Il ne décide pas quel ensemble de règles appliquer selon le contexte. Les deux se complètent : les validators sont les briques, les stratégies les assemblent.
Peut-on changer de stratégie sans recréer le formulaire ?
Oui. La méthode apply() de la factory ajoute ou retire les champs, met à jour les validators puis appelle updateValueAndValidity(). Les valeurs déjà saisies sont conservées.
Peut-on combiner le pattern Strategy avec NgRx ou les signals ?
Oui. Le rôle ou le feature flag provient d’un sélecteur NgRx ou d’un signal, et un effect() réapplique la stratégie correspondante à chaque changement.
Comment tester une stratégie ?
Instanciez la stratégie, créez un FormControl avec ses validators et vérifiez hasError(). Aucun TestBed ni composant n’est nécessaire.
Est-ce compatible avec les FormArray ?
Oui. Étendez l’interface avec une méthode qui décrit les tableaux (par exemple une liste d’adresses) et laissez la factory créer les FormArray correspondants.
Le pattern Strategy a-t-il sa place dans une architecture DDD côté frontend ?
Oui. Chaque stratégie rend explicite une règle métier d’un contexte donné, ce qui s’aligne naturellement avec la notion de bounded context.
Pour aller plus loin
Ce pattern est un exemple concret du principe Open/Closed. Retrouvez-le, avec les autres principes SOLID appliqués à des cas e-commerce, dans nos formations Angular et Clean Code.




