Retour aux articles

Développement de formulaires réactifs dynamiques avec validateurs personnalisés en Angular pour ERP

Développement de formulaires réactifs dynamiques avec validateurs personnalisés en Angular pour ERP | Laty Gueye Samba - Développeur Full Stack Dakar Sénégal, Expert Java Spring Boot Angular
```html

Développement de formulaires réactifs dynamiques avec validateurs personnalisés en Angular pour ERP

Dans les ERP modernes, les formulaires constituent un point critique : saisies complexes, règles métier évolutives, contraintes par entité (clients, articles, commandes), et comportements conditionnels. Angular, grâce aux Reactive Forms, permet de construire des écrans robustes, testables et maintenables, tout en intégrant des validateurs personnalisés adaptés aux besoins spécifiques de l’organisation.

Pourquoi des formulaires réactifs dynamiques pour un ERP

Les ERP exigent souvent :

  • Un modèle de formulaire piloté par configuration (métadonnées, schémas, rôles).
  • Des validations métier complexes (cohérence entre champs, règles conditionnelles, contrôles d’état).
  • Une adaptation en temps réel (ajout/suppression de lignes, champs optionnels, changements de workflow).
  • Une évolutivité sans réécriture massive des composants.

Les Reactive Forms apportent une structure idéale : la logique de validation et l’état du formulaire sont centralisés, synchronisés et facilement testables.

Architecture recommandée

Séparer la définition du formulaire et la logique métier

Une approche maintenable consiste à distinguer :

  • Le schéma de formulaire (décrit les champs, types, règles, dépendances).
  • Le générateur (construit FormGroup et FormArray à partir du schéma).
  • Les validateurs personnalisés (implémentent les règles métier).
  • Les composants d’interface (affichent erreurs, états, et gèrent l’interaction).

Exemple de schéma configuré

Le schéma peut provenir d’un backend (ou être stocké en JSON) afin d’autoriser des règles dynamiques par module ERP.

<!-- Exemple simplifié (pseudo-JSON) --> { "fields": [ { "name": "customerId", "type": "select", "required": true }, { "name": "invoiceDate", "type": "date", "required": true }, { "name": "dueDate", "type": "date", "required": true, "validators": ["afterOrEqual:invoiceDate"] } ], "lineItems": { "repeatable": true, "minItems": 1, "itemFields": [ { "name": "productCode", "type": "text", "required": true }, { "name": "quantity", "type": "number", "required": true, "validators": ["positive"] } ] } }

Génération dynamique d’un FormGroup et d’un FormArray

Création du FormGroup à partir d’un schéma

Le générateur convertit les métadonnées en structure Angular. Les validateurs peuvent être attachés selon les règles déclarées.

import { FormBuilder, FormGroup, Validators, FormArray } from '@angular/forms'; type FieldConfig = { name: string; required?: boolean; validators?: string[]; }; type FormSchema = { fields: FieldConfig[]; lineItems?: { repeatable: boolean; minItems?: number; itemFields: FieldConfig[]; }; }; export class DynamicFormFactory { constructor(private fb: FormBuilder) {} createForm(schema: FormSchema): FormGroup { const groupConfig: Record<string, any> = {}; for (const f of schema.fields) { const validators = []; if (f.required) validators.push(Validators.required); // Ajout de validateurs personnalisés selon déclaration if (f.validators?.length) { // Exemple: traitement plus sophistiqué selon format for (const rule of f.validators) { validators.push(this.mapValidator(rule)); } } groupConfig[f.name] = [null, validators]; } if (schema.lineItems?.repeatable) { groupConfig['lineItems'] = this.fb.array( [], schema.lineItems.minItems ? [this.minItems(schema.lineItems.minItems)] : [] ); } return this.fb.group(groupConfig); } private mapValidator(rule: string) { // Exemples de mapping // - "positive" // - "afterOrEqual:invoiceDate" if (rule === 'positive') return this.positiveNumber(); if (rule.startsWith('afterOrEqual:')) { const refField = rule.split(':')[1]; return this.afterOrEqual(refField); } return null; } private positiveNumber() { return (control: any) => { const value = control.value; if (value == null) return null; return value > 0 ? null : { positive: true }; }; } private afterOrEqual(refField: string) { return (control: any) => { const parent = control?.parent as FormGroup | null; if (!parent) return null; const ref = parent.get(refField)?.value; const current = control.value; if (!ref || !current) return null; return new Date(current) >= new Date(ref) ? null : { afterOrEqual: { refField } }; }; } private minItems(min: number) { return (array: FormArray) => { return array.length >= min ? null : { minItems: { required: min, actual: array.length } }; }; } }

Ajout dynamique de lignes (FormArray)

Les ERP contiennent fréquemment des tableaux éditables : lignes de facture, composants de nomenclature, opérations d’un planning, etc.

import { FormArray, FormBuilder, Validators, FormGroup } from '@angular/forms'; export class LineItemsBuilder { constructor(private fb: FormBuilder) {} createLineItem(itemFields: FieldConfig[]): FormGroup { const config: Record<string, any> = {}; for (const f of itemFields) { const validators = []; if (f.required) validators.push(Validators.required); if (f.validators?.length) { for (const rule of f.validators) { // Ici, le mapping doit idéalement réutiliser la même logique de validateurs if (rule === 'positive') validators.push((c: any) => (c.value > 0 ? null : { positive: true })); } } config[f.name] = [null, validators]; } return this.fb.group(config); } addLineItem(array: FormArray, itemFields: FieldConfig[]) { array.push(this.createLineItem(itemFields)); } }

Validateurs personnalisés : patterns efficaces

Validation inter-champs (cross-field)

Beaucoup de règles ERP nécessitent une comparaison entre champs : dates, totaux, montants, dépendances entre statut et champs obligatoires.

Exemple : dueDate doit être postérieure ou égale à invoiceDate.

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; export function afterOrEqualValidator(refField: string): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { const parent = control.parent as AbstractControl | null; if (!parent) return null; const ref = parent.get(refField)?.value; const current = control.value; if (!ref || !current) return null; return new Date(current) >= new Date(ref) ? null : { afterOrEqual: { refField } }; }; }

Validation dépendante du contexte (rôle, module, statut)

Dans un ERP, certaines règles varient selon le workflow. Un validateur peut intégrer des paramètres (ex. rôle utilisateur, code entité, état).

import { AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; export function requiredIfValidator(isRequired: () => boolean): ValidatorFn { return (control: AbstractControl): ValidationErrors | null => { if (!isRequired()) return null; const v = control.value; return v == null || v === '' ? { requiredIf: true } : null; }; }

Encapsulation en services et réutilisation

Les validateurs peuvent être centralisés dans une bibliothèque interne (ex. validators/) afin d’éviter la duplication dans les modules ERP.

La réutilisation facilite aussi :

  • les tests unitaires,
  • la cohérence des messages d’erreur,
  • l’alignement des règles métier entre formulaires.

Gestion des erreurs et de l’UX

Une validation robuste n’est utile que si elle est présentée clairement. Les meilleures pratiques incluent :

  • Messages d’erreur par erreur (positive, afterOrEqual, minItems...).
  • Déclenchement contrôlé : erreurs affichées après touched ou dirty.
  • Indicateurs de cohérence : style, icônes, focus sur champ en erreur.

Exemple d’affichage des erreurs en Angular

import { Component } from '@angular/core'; import { FormGroup } from '@angular/forms'; @Component({ selector: 'app-invoice-form', template: ` <form [formGroup]="form"> <label>Date de facture</label> <input type="date" formControlName="invoiceDate" /> <div *ngIf="form.get('invoiceDate')?.touched && form.get('invoiceDate')?.hasError('required')"> La date de facture est obligatoire. </div> <label>Échéance</label> <input type="date" formControlName="dueDate" /> <div *ngIf="form.get('dueDate')?.touched && form.get('dueDate')?.hasError('afterOrEqual')"> L’échéance doit être postérieure ou égale à la date de facture. </div> <button type="submit" [disabled]="form.invalid">Valider</button> </form> ` }) export class InvoiceFormComponent { form!: FormGroup; }

Performance et fiabilité

Les formulaires dynamiques peuvent devenir coûteux si la logique est mal structurée. Pour éviter les lenteurs :

  • Limiter les recalculs : privilégier la validation au moment des changements pertinents.
  • Éviter les validateurs excessivement couplés aux structures DOM.
  • Utiliser des méthodes pures quand possible.
  • Tester systématiquement les règles métier avec des cas limites (dates, arrondis, montants, listes vides).

Tests unitaires : garantir la conformité métier

Pour un ERP, la régression est un risque majeur. Les validateurs personnalisés doivent être testés :

  • validation de base (succès/échec),
  • cas limites (valeurs vides, format invalide),
  • cas inter-champs (mise à jour du parent, ordre de saisie).
import { FormBuilder, Validators } from '@angular/forms'; import { afterOrEqualValidator } from './after-or-equal.validator'; describe('afterOrEqualValidator', () => { it('should fail when dueDate is before invoiceDate', () => { const fb = new FormBuilder(); const form = fb.group({ invoiceDate: ['2026-01-10'], dueDate: ['2026-01-09', [afterOrEqualValidator('invoiceDate')]] }); const due = form.get('dueDate'); due?.updateValueAndValidity(); expect(due?.errors?.['afterOrEqual']).toBeTruthy(); }); it('should pass when dueDate is equal to invoiceDate', () => { const fb = new FormBuilder(); const form = fb.group({ invoiceDate: ['2026-01-10'], dueDate: ['2026-01-10', [afterOrEqualValidator('invoiceDate')]] }); const due = form.get('dueDate'); due?.updateValueAndValidity(); expect(due?.errors).toBeNull(); }); });

Conclusion

En environnement ERP, le développement de formulaires réactifs dynamiques avec Angular s’avère particulièrement pertinent pour gérer la complexité des règles métier. La combinaison de Reactive Forms, de FormArray pour les listes éditables et de validateurs personnalisés fournit une base solide : maintenable, testable et extensible. Une architecture orientée schéma et validateurs réutilisables renforce la capacité d’adaptation aux évolutions fonctionnelles.

À propos de l'auteur

Laty Gueye Samba est développeur Full Stack basé à Dakar, Sénégal. Spécialiste des écosystèmes Java / Spring Boot et Angular.

Contact : latygueyesamba@gmail.com  |  Dakar, Sénégal

© 2026 Laty Gueye Samba.