@suntelecoms/ngx-dynamic-form
v1.7.19
Published
Générateur de formulaires Angular dynamiques — SUNTELECOMS
Downloads
1,288
Maintainers
Readme
@suntelecoms/ngx-dynamic-form
Génère des formulaires Angular complexes à partir d'une configuration TypeScript — sans écrire une ligne de template HTML. Inclut un Form Builder visuel, la gestion des schémas sauvegardés, un système de stockage interchangeable (localStorage / API REST) et le support des formulaires multi-étapes (Stepper).
Sommaire
- Présentation
- Fonctionnalités
- Installation
- Démarrage rapide — formulaire statique
- Démarrage rapide — stack complète
- Types de champs
- Configuration — DynamicFormConfig
- Propriétés d'un champ — DynamicFormField
- Options dynamiques depuis une API
- Listes en cascade — dependsOn
- Recherche distante (typeahead) — remoteSearch
- Filtre des options API — optionsFilter
- Validation
- Validation par pattern — regex
- Vérification d'unicité — uniqueCheck
- Affichage conditionnel — showWhen
- Auto-remplissage depuis une API — lookup
- Required conditionnel — requiredWhen
- Validation croisée — maxField / minField
- Au moins un champ obligatoire — atLeastOneOf
- Guard de step — beforeNext
- Icônes (Material Icons)
- Disposition en grille (col)
- API du composant DynamicForm
- Formulaires multi-étapes — Stepper
- provideNgxForms — configuration globale
- FormConfigService — gestion des schémas
- Stockage — LocalStorage vs API REST
- Form Builder — éditeur visuel
- Composants de navigation
- Intégration dans un composant
- Pages de démonstration
- Exemples complets
- Personnalisation CSS
- Interfaces TypeScript
- Auteur
Présentation
@suntelecoms/ngx-dynamic-form est un plugin Angular 19 qui génère des formulaires réactifs dynamiquement à partir d'un objet de configuration TypeScript. Il s'appuie sur Angular Reactive Forms et Angular Material.
Depuis la v1.2.0, le plugin embarque :
- Le Form Builder visuel (
<ngx-form-builder>) — avec mode Stepper - Le gestionnaire de formulaires (
<ngx-mes-formulaires>) - La page de rendu par code (
<ngx-form-loader>) - Un système de stockage abstrait (localStorage par défaut, API REST Spring en option)
- Les formulaires multi-étapes (
<ngx-stepper-form>) avec étape de récapitulatif
Résultat : dans n'importe quel projet Angular, il suffit de 2 lignes pour avoir une interface complète de paramétrage de formulaires.
Fonctionnalités
- 25 types de champs —
text,email,password,number,tel,url,date,datetime-local,time,month,week,textarea,select,multiselect,radio,checkbox,switch,range,color,location,file,phone,hidden,divider,heading - 3 variantes d'apparence —
outline,fill,simple(label fixe au-dessus) — globale ou par champ (v1.2.7) - Datepicker Material (
mat-datepicker) pour le typedate(v1.2.8) - Géolocalisation — type
locationavec bouton GPS et APInavigator.geolocation(v1.2.8) - Curseur de valeur — type
rangerendu enmat-slider(v1.2.8) - Interrupteur — type
switchrendu enmat-slide-toggle(v1.2.8) - Styling par champ —
fontSize,textAlign,color,backgroundColor,borderColor,labelColor,fontWeight,fontStyle,cssClass,styles(v1.2.7) - Masque de saisie — propriété
mask(#= chiffre, autres chars = séparateurs) (v1.2.7) - Préfixe / suffixe texte — propriétés
prefixetsuffix(v1.2.7) - Code unique par formulaire — propriété
codesaisie par l'utilisateur, utilisée pour l'URL de partage et la récupération par code (v1.2.9) - Authentification Bearer token —
authdansprovideNgxForms+ intercepteurngxFormsAuthInterceptorpour les APIs sécurisées (login/mot de passe → JWT) (v1.3.0) - Floating label Angular Material (
mat-form-field appearance="outline") - Validation déclarative —
required,email,minLength,maxLength,min,max,pattern, validateur personnalisé - Messages d'erreur personnalisés par champ
- Champ téléphone international — type
phone, sélecteur de pays (menu avec recherche, apparence Material native) + masque automatique, valeur E.164 dans le formulaire, 135 pays préconfigurés (v1.4.0, sélecteur revu en v1.4.7) - Vérification d'unicité asynchrone — propriété
uniqueChecksur un champ, appel API debounced (400 ms), auto-détection enveloppe SUNTELECOMS{ data: boolean }(v1.4.0) - Affichage conditionnel (
showWhen) avec 7 opérateurs - Logique ET / OU sur conditions multiples — propriété
conditionLogic: 'AND' | 'OR', configurable depuis le Builder (v1.4.0) - Grille 12 colonnes — propriété
col - Icônes Material Icons — préfixe et suffixe par champ
- Préremplissage via
[initialValues] - Mode lecture seule / désactivé par champ
- Sections et séparateurs (
heading,divider) - API publique —
getForm(),patchValues()via@ViewChild - Événements —
formSubmit,formChange,formReset - Formulaires multi-étapes (Stepper) —
<ngx-stepper-form>avec navigation, validation par étape et récapitulatif automatique (v1.2.0) - Form Builder visuel — interface 3 panneaux, supporte les modes Formulaire et Stepper (v1.2.0)
- Options dynamiques depuis une API — propriétés
url,displayName,displayValuesur les champsselect,multiselect,radio(v1.2.5), avec extraction automatique des réponses enveloppées ({ data: [...] }) ou viaresponseDataKey(v1.4.4) - Listes en cascade — propriété
dependsOn: un champ recharge ses options (statiques ou distantes, avec interpolation{{champ}}dansurl) quand un autre champ change (v1.4.5) - Stockage abstrait — localStorage ou API REST Spring
- FormConfigService — Signal-based, Observable API
- Auto-remplissage depuis une API (
lookup) — surblur, requête GET/POST → remplit des champs cibles via une mappopulateen notation pointée (v1.6.0) - Required conditionnel (
requiredWhen) —Validators.requiredactivé dynamiquement selon uneFieldCondition, sans code impératif (v1.6.0) - Validation croisée min/max (
maxField/minField) — erreur posée sur leFormControlcible si la valeur dépasse/est inférieure à celle d'un autre champ (v1.6.0) - Au moins un obligatoire (
atLeastOneOf) — validateur de groupe surDynamicFormConfigetFormStep(v1.6.0) - Guard de step asynchrone (
beforeNext) — appel HTTP avant l'avancement ; HTTP 4xx bloque l'étape et affiche le message d'erreur du serveur (v1.6.0) - Événement
fieldChange— émis parDynamicFormComponentetStepperFormComponentà chaque changement de champ, avec{ key, value, form }pour piloter la logique métier depuis l'hôte (v1.6.0) - Copie automatique de champ (
copyFrom) — propriété surDynamicFormField: quand le champ source change, la valeur est copiée automatiquement dans ce champ sans code impératif dans l'hôte (v1.7.2) - Valeur par défaut (
defaultValue) — initialise le champ avec une valeur arbitraire à l'ouverture du formulaire (v1.7.2) - Form Builder complet — le builder visuel expose désormais toutes les propriétés de
DynamicFormField:defaultValue,disabled,hidden,copyFrom,responseDataKey,dependsOn/dependsOnMode/dependsOnItemKey/dependsOnOffset,minDate/maxDate,lookup.triggerOn(v1.7.3) - Recherche distante (typeahead) — propriété
remoteSearchsur un champselect: au lieu de charger toutes les options viaurl, affiche unmat-autocompletequi interrogeurlavec le texte tapé (debounce,minCharsavant la première requête, params statiques additionnels) — pour les listes trop grandes pour être chargées entièrement (ex : recherche client) (v1.7.5) - Locale du datepicker configurable — le type
dateafficheDD/MM/YYYYpar défaut (fr-FR, calendrier et noms de mois en français) au lieu du format US du navigateur ; personnalisable par champ (dateLocale) ou globalement (provideNgxForms({ dateLocale })) (v1.7.6) - Recherche dans « Mes Formulaires » — filtre les formulaires sauvegardés par titre ou code, utile dès que le catalogue grandit (v1.7.9)
- Filtre des options API — propriété
optionsFiltersur un champselect/multiselect/radiochargé viaurl: ne garde que les items dont une propriété correspond à une condition (ex:status === 'ACTIF'), avec logique ET/OU sur plusieurs conditions — même mécanique queshowWhen, mais évaluée contre les propriétés brutes de chaque item API (v1.7.10) - Champ fichier fonctionnel — le type
fileest maintenant relié au formulaire (dropzone clic/glisser-déposer, liste de fichiers choisis,formControl.value=File/File[]) ; avant v1.7.11 l'<input type="file">n'était pas connecté, aucune valeur ne remontait (v1.7.11) - Code ISO pays — type
iso-country: liste déroulante recherchable (nom ou code) avec drapeau, même référentiel quecountry-dial-code— garantit un code toujours valide au lieu d'une saisie libre (v1.7.12, converti en liste déroulante en v1.7.14) - Dates au calendrier uniquement — le type
datene se saisit plus au clavier : un clic ouvre le calendrier (Material ou sélecteur natif en apparencesimple), ce qui évite aussi la lecture US MM/JJ d'une date tapée (v1.7.19) - Période de dates —
maxField/minFieldsur les champsdate(etdatetime-local,month,week,time) : date de début ≤ date de fin, dates hors période grisées dans le calendrier, configurable dans le Form Builder (v1.7.18) - Label composé —
displayNameaccepte un chemin pointé (personnePhysique.nom) ou un modèle de concaténation ('{{codeClient}} - {{telephone}}') pour les options chargées viaurl(y comprisremoteSearch) (v1.7.17) - Indicatif téléphonique pays — nouveau type
country-dial-code: recherche de pays (nom/indicatif/code) avec drapeau, stocke uniquement l'indicatif choisi (ex:'+221') comme attribut autonome, réutilise le référentiel du typephone(v1.7.13)
Installation
1. Prérequis
- Angular 19+
- Node.js 18+
2. Installer les dépendances
npm install @suntelecoms/ngx-dynamic-form @angular/material@^19.0.0 @angular/cdk@^19.0.03. Ajouter le thème Material et les icônes
angular.json :
"styles": [
"@angular/material/prebuilt-themes/azure-blue.css",
"src/styles.css"
]index.html :
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">4. Configurer le plugin (app.config.ts)
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideAnimations } from '@angular/platform-browser/animations';
import { provideNgxForms } from '@suntelecoms/ngx-dynamic-form';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
provideAnimations(),
provideNgxForms({ storage: 'local' }), // ← stockage localStorage
],
};5. Ajouter les routes (app.routes.ts)
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'builder',
loadComponent: () => import('@suntelecoms/ngx-dynamic-form').then(m => m.FormBuilderComponent),
},
{
path: 'mes-formulaires',
loadComponent: () => import('@suntelecoms/ngx-dynamic-form').then(m => m.MesFormulairesComponent),
},
{
path: 'form/:code', // ← le formulaire est identifié par son code unique
loadComponent: () => import('@suntelecoms/ngx-dynamic-form').then(m => m.FormLoaderComponent),
},
];C'est tout. Aucun code supplémentaire à écrire — le Builder, la liste des formulaires et la page de rendu sont entièrement fournis par le plugin.
Démarrage rapide — formulaire statique
Utilisez <ngx-dynamic-form> directement avec une config TypeScript :
import { Component } from '@angular/core';
import { DynamicFormComponent } from '@suntelecoms/ngx-dynamic-form';
import type { DynamicFormConfig, DynamicFormSubmitEvent } from '@suntelecoms/ngx-dynamic-form';
@Component({
selector: 'app-contact',
standalone: true,
imports: [DynamicFormComponent],
template: `
<ngx-dynamic-form [config]="config" (formSubmit)="onSubmit($event)" />
`,
})
export class ContactComponent {
config: DynamicFormConfig = {
submitLabel: 'Envoyer',
showReset: true,
fields: [
{ key: 'name', type: 'text', label: 'Nom complet', col: 6, icon: 'person',
validation: { required: true } },
{ key: 'email', type: 'email', label: 'E-mail', col: 6, icon: 'email',
validation: { required: true, email: true } },
{ key: 'message', type: 'textarea', label: 'Message', rows: 5,
validation: { required: true } },
],
};
onSubmit(event: DynamicFormSubmitEvent): void {
console.log(event.value);
}
}Démarrage rapide — stack complète
Pour disposer du Form Builder + gestionnaire + rendu par URL dans votre application, il suffit des 2 fichiers de configuration vus à l'installation. Après ng serve :
| URL | Page |
|---|---|
| /builder | Interface graphique de création de formulaires (simple ou stepper) |
| /mes-formulaires | Liste des formulaires sauvegardés, avec rendu à la volée |
| /form/mon-code | Rendu d'un formulaire sauvegardé par son code unique |
Types de champs
Saisie texte
| Type | Rendu | Description |
|---|---|---|
| text | <input type="text"> | Champ texte standard |
| email | <input type="email"> | Validation e-mail intégrée |
| password | <input type="password"> | Mot de passe masqué |
| number | <input type="number"> | Supporte min, max |
| tel | <input type="tel"> | Téléphone — compatible masque de saisie |
| url | <input type="url"> | URL |
| textarea | <textarea> | Zone de texte multi-lignes, propriété rows |
Date & Heure
| Type | Rendu | Description |
|---|---|---|
| date | mat-datepicker | Calendrier Material — popup avec sélection mois/année (v1.2.8) ; minDate/maxDate pour borner les dates sélectionnables, 'today' ou une date ISO (v1.6.6) ; affichage DD/MM/YYYY (locale fr-FR) par défaut, personnalisable par champ (dateLocale) ou globalement (provideNgxForms({ dateLocale })) (v1.7.6) ; sélection au calendrier uniquement — saisie clavier bloquée, un clic sur le champ ouvre le calendrier, le bouton calendrier reste accessible au clavier (Tab puis Entrée) (v1.7.19) |
| datetime-local | <input type="datetime-local"> | Date et heure combinées |
| time | <input type="time"> | Heure (HH:mm) |
| month | <input type="month"> | Mois et année |
| week | <input type="week"> | Semaine ISO |
Choix
| Type | Rendu | Description |
|---|---|---|
| select | <mat-select> | Liste déroulante — 1 choix — options statiques ou API |
| multiselect | <mat-select multiple> | Liste déroulante — plusieurs choix |
| radio | <mat-radio-group> | Boutons radio — options statiques ou API |
| checkbox | <mat-checkbox> | Case à cocher booléenne |
| switch | <mat-slide-toggle> | Interrupteur on/off (v1.2.8) |
Spéciaux
| Type | Rendu | Description |
|---|---|---|
| range | <mat-slider> | Curseur de valeur — propriétés min, max, step, showValue (v1.2.8) |
| color | <input type="color"> | Sélecteur de couleur avec affichage du code hex (v1.2.8) |
| location | <input> + bouton GPS | Champ adresse avec géolocalisation — bouton my_location déclenche navigator.geolocation (v1.2.8) |
| file | Dropzone (clic ou glisser-déposer) + liste de fichiers choisis | Téléversement — accept, multiple ; formControl.value = File (ou File[] si multiple), à empaqueter en FormData par le consommateur dans onSubmit — le plugin ne fait aucun appel HTTP lui-même (fonctionnel depuis v1.7.11 — avant cette version l'<input> n'était pas relié au formulaire) |
| phone | mat-form-field + menu pays | Téléphone international — sélecteur de pays (menu + recherche), masque automatique, valeur E.164 dans le FormGroup (v1.4.0, v1.4.7) |
| iso-country | mat-autocomplete — recherche pays | Code ISO 3166-1 alpha-2 : liste déroulante recherchable (nom ou code) avec drapeau SVG (flag-icons, même rendu que phone — pas d'emoji, repli en deux lettres nues sur certains Windows/webviews), sur le même référentiel PHONE_COUNTRIES que country-dial-code (son .code EST le code ISO) — garantit un code toujours valide, pas de saisie libre ; validation ^[A-Za-z]{2}$ conservée en filet de sécurité si une valeur est posée par programmation (v1.7.12 en saisie libre, converti en liste déroulante en v1.7.14, drapeau SVG en v1.7.15) |
| country-dial-code | mat-autocomplete — recherche pays | Choix de l'indicatif téléphonique d'un pays comme attribut autonome (ex: Pays.indicatifTel = '+221') — distinct de phone (qui saisit un numéro complet E.164) ; recherche par nom, indicatif ou code sur le même référentiel PHONE_COUNTRIES, drapeau SVG (flag-icons, même rendu que phone) dans chaque résultat, formControl.value = l'indicatif choisi (ex: '+221') (v1.7.13, drapeau SVG en v1.7.15) |
Mise en page
| Type | Rendu | Description |
|---|---|---|
| hidden | <input type="hidden"> | Champ caché (présent dans le FormGroup) |
| divider | <hr> | Séparateur horizontal |
| heading | <h1> à <h6> | Titre de section — propriétés text, level |
Configuration — DynamicFormConfig
interface DynamicFormConfig {
fields: DynamicFormField[]; // Liste des champs (obligatoire)
submitLabel?: string; // Libellé du bouton Envoyer (défaut : "Envoyer")
resetLabel?: string; // Libellé du bouton Réinitialiser (défaut : "Réinitialiser")
showReset?: boolean; // Afficher le bouton Réinitialiser (défaut : false)
layout?: 'vertical' | 'horizontal' | 'inline';
cssClass?: string;
debug?: boolean; // Affiche les valeurs JSON en temps réel
// Apparence globale ← v1.2.7
appearance?: 'outline' | 'fill' | 'simple'; // défaut : 'outline'
floatLabel?: 'auto' | 'always'; // défaut : 'auto'
// Validation de groupe ← v1.6.0
atLeastOneOf?: string[]; // Au moins un des champs listés doit être renseigné
atLeastOneOfMessage?: string; // Message d'erreur (défaut : 'Au moins un champ est requis.')
}Propriétés d'un champ — DynamicFormField
interface DynamicFormField {
// Obligatoire
key: string; // Identifiant unique (clé dans FormGroup)
type: FieldType;
// Affichage
label?: string;
placeholder?: string;
hint?: string;
icon?: string; // Icône préfixe (Material Icons)
iconSuffix?: string; // Icône suffixe (Material Icons)
prefix?: string; // Texte préfixe dans le champ (ex : '+221') ← v1.2.7
suffix?: string; // Texte suffixe dans le champ (ex : 'FCFA') ← v1.2.7
// Valeur & état
defaultValue?: any;
disabled?: boolean;
readonly?: boolean;
hidden?: boolean;
// Options statiques (select / multiselect / radio)
options?: SelectOption[];
// Options dynamiques chargées depuis une API ← v1.2.5
url?: string; // endpoint relatif ('villes'), absolu ('https://...'),
// ou racine-relatif ('/autre-service/api/...') ← v1.4.4
displayName?: string; // label : clé, chemin pointé ou modèle '{{codeClient}} - {{telephone}}' (défaut : 'label')
displayValue?: string; // propriété de la réponse API utilisée comme valeur (défaut : 'value')
responseDataKey?: string; // clé du tableau si la réponse est enveloppée, ex: 'data'
// (défaut : essaie 'data' automatiquement si la réponse
// n'est pas déjà un tableau) ← v1.4.4
// Recherche distante : remplace le chargement eager de `url` par un typeahead
// debouncé — voir Recherche distante (typeahead) — remoteSearch ← v1.7.5
remoteSearch?: RemoteSearchConfig; // type: 'select' uniquement
// Filtre les items reçus de `url` avant mapping — voir Filtre des options API — optionsFilter ← v1.7.10
optionsFilter?: FieldCondition | FieldCondition[];
optionsFilterLogic?: 'AND' | 'OR'; // défaut 'AND'
// Copie automatique depuis un autre champ ← v1.7.2
copyFrom?: string; // clé du champ source — quand sa valeur change, elle est copiée ici automatiquement
// Liste en cascade : recharge ses options quand le champ `dependsOn` change ← v1.4.5
dependsOn?: string; // clé d'un autre champ du formulaire
dependsOnMode?: 'server' | 'client'; // défaut 'server' — voir Listes en cascade ← v1.4.6
dependsOnItemKey?: string; // mode 'client' : propriété de l'item à comparer ← v1.4.6
dependsOnOffset?: number; // mode 'client' : décalage numérique avant comparaison ← v1.4.6
// Validation
validation?: FieldValidation;
// Disposition
col?: 1|2|3|4|6|8|9|12; // Colonnes sur 12 (défaut : 12)
cssClass?: string; // Classe CSS ajoutée sur l'hôte ← v1.2.7
// Affichage conditionnel
showWhen?: FieldCondition | FieldCondition[];
conditionLogic?: 'AND' | 'OR'; // Logique entre conditions multiples (défaut : 'AND') ← v1.4.0
// Vérification d'unicité asynchrone ← v1.4.0
uniqueCheck?: {
url: string; // Endpoint POST — reçoit { value } — doit retourner boolean ou { data: boolean }
message?: string; // Message d'erreur (défaut : 'Cette valeur existe déjà.')
};
// Auto-remplissage depuis une API ← v1.6.0
lookup?: FieldLookup;
// Spécifique phone ← v1.4.0
defaultCountry?: string; // Code ISO 3166-1 alpha-2 (ex : 'FR', 'SN') — défaut : 'FR'
// Spécifique file
accept?: string; // Ex : ".pdf,.jpg"
multiple?: boolean;
// Spécifique textarea
rows?: number; // Défaut : 3
// Spécifique date ← v1.6.6
minDate?: string; // 'today', ou une date ISO ('2026-01-01')
maxDate?: string; // idem — borne le mat-datepicker et l'input natif (apparence simple)
dateLocale?: string; // Locale d'affichage (ex: 'fr-FR' → 25/08/2026) — remplace la
// locale globale de provideNgxForms. Défaut global : 'fr-FR' ← v1.7.6
// Exemple — une date de mise en circulation ne peut pas être dans le futur :
// { key: 'dateMiseCirculation', type: 'date', label: 'Date de mise en circulation', maxDate: 'today' }
// Une date de rendez-vous ne peut pas être dans le passé :
// { key: 'dateRdv', type: 'date', label: 'Date du rendez-vous', minDate: 'today' }
// Spécifique heading
text?: string;
level?: 1|2|3|4|5|6; // Défaut : 2
// Spécifique range ← v1.2.8
min?: number; // Valeur minimale du curseur (défaut : 0)
max?: number; // Valeur maximale du curseur (défaut : 100)
step?: number; // Pas du curseur (défaut : 1)
showValue?: boolean; // Afficher la valeur courante à côté du curseur
// Masque de saisie ← v1.2.7
// '#' = chiffre obligatoire, tout autre caractère = séparateur littéral
// Ex : '## ### ## ##' → téléphone SN, '##/##/####' → date, '#### #### #### ####' → CB
mask?: string;
// Apparence individuelle (override la valeur globale du formulaire) ← v1.2.7
appearance?: 'outline' | 'fill' | 'simple';
floatLabel?: 'auto' | 'always';
// Style & Apparence ← v1.2.7 / v1.2.8
fontSize?: string; // Ex : '14px', '1.1rem'
textAlign?: 'left' | 'center' | 'right';
color?: string; // Couleur du texte saisi
backgroundColor?: string; // Couleur de fond du champ
borderColor?: string; // Couleur de la bordure ← v1.2.8
labelColor?: string; // Couleur du label ← v1.2.8
fontWeight?: 'normal' | 'bold';
fontStyle?: 'normal' | 'italic';
styles?: Record<string, string>; // Styles CSS inline supplémentaires
attrs?: Record<string, any>;
}SelectOption
interface SelectOption {
label: string;
value: any;
disabled?: boolean;
group?: string;
dependsOnValue?: any; // valeur du champ `dependsOn` pour laquelle cette option apparaît ← v1.4.5
}Options dynamiques depuis une API
Nouveauté v1.2.5
Pour les champs select, multiselect et radio, les options peuvent être chargées automatiquement depuis une API REST au lieu d'être définies statiquement. Il suffit d'utiliser url à la place de options.
Comment ça fonctionne
- Le composant détecte la propriété
urlsur le champ - Il effectue un
GETsur l'URL complète (dataUrl+url) - Si la réponse n'est pas déjà un tableau, il essaie de l'extraire depuis
responseDataKey(ou'data'par défaut) — voir Réponse API attendue ← v1.4.4 - Il mappe chaque objet du tableau en
{ label, value }en utilisantdisplayNameetdisplayValue - Pendant le chargement, le select affiche "Chargement…" avec un spinner
Configuration dans app.config.ts
import { provideHttpClient } from '@angular/common/http';
import { provideNgxForms } from '@suntelecoms/ngx-dynamic-form';
providers: [
provideHttpClient(), // requis pour les options depuis API
provideNgxForms({
storage: 'local',
dataUrl: 'https://api.monprojet.com', // base URL pour les options
}),
]Exemples
Options depuis une API (URL relative) :
{
key: 'ville',
type: 'select',
label: 'Ville',
col: 6,
icon: 'location_city',
url: 'villes', // → GET https://api.monprojet.com/villes
displayName: 'nom', // propriété affichée dans la liste
displayValue: 'id', // propriété utilisée comme valeur du formulaire
validation: { required: true },
}Options depuis une API (URL absolue) :
{
key: 'compte',
type: 'select',
label: 'Compte',
col: 6,
url: 'https://autre-api.com/comptes', // URL absolue — dataUrl ignoré
displayName: 'numero',
displayValue: 'numero',
validation: { required: true },
}Options depuis un backend différent, même origine (chemin racine-relatif) : ← v1.4.4
{
key: 'role',
type: 'select',
label: 'Rôle',
col: 12,
// Commence par '/' → traité comme dataUrl : résolu par le navigateur
// contre l'origine courante (donc via le même proxy/reverse-proxy déjà
// configuré pour cette autre API), sans coder en dur un hôte complet
// qui ne serait correct que dans un seul environnement.
url: '/tp-security-service/api/roles?size=1000&page=0',
responseDataKey: 'data',
displayName: 'nom',
displayValue: 'nom',
validation: { required: true },
}Multi-sélection depuis une API :
{
key: 'categories',
type: 'multiselect',
label: 'Catégories',
col: 12,
url: 'categories',
displayName: 'libelle',
displayValue: 'code',
}Réponse API attendue
L'API peut retourner soit un tableau JSON brut :
[
{ "id": 1, "nom": "Dakar" },
{ "id": 2, "nom": "Thiès" },
{ "id": 3, "nom": "Saint-Louis" }
]soit une réponse enveloppée (convention courante des API paginées : { data, total, ... }) :
{
"data": [
{ "id": 1, "nom": "Dakar" },
{ "id": 2, "nom": "Thiès" },
{ "id": 3, "nom": "Saint-Louis" }
],
"total": 3
}Dans ce second cas, le tableau est automatiquement extrait depuis la clé 'data' — aucune configuration requise. Si l'API utilise une autre clé ('items', 'results', 'list'…), indiquez-la explicitement avec responseDataKey ← v1.4.4 :
{
key: 'ville',
type: 'select',
url: 'villes',
responseDataKey: 'items', // → extrait response.items au lieu de response.data
displayName: 'nom',
displayValue: 'id',
}Avec displayName: 'nom' et displayValue: 'id', le select affichera Dakar, Thiès, Saint-Louis et soumettra 1, 2, 3.
Résumé des propriétés
| Propriété | Type | Description |
|---|---|---|
| url | string | Endpoint relatif ('villes'), absolu ('https://...') ou racine-relatif ('/autre-service/...', résolu contre l'origine courante) ← v1.4.4. Quand défini, options est ignoré. |
| displayName | string | Propriété de l'objet API utilisée comme label affiché (défaut : 'label'). Accepte un chemin pointé ('personnePhysique.nom') ou un modèle de concaténation ('{{codeClient}} - {{telephone}}') — voir Label composé ← v1.7.17 |
| displayValue | string | Propriété de l'objet API utilisée comme valeur du formulaire (défaut : 'value') |
| responseDataKey | string | Clé du tableau si la réponse est enveloppée (défaut : essaie 'data' automatiquement) ← v1.4.4 |
| dependsOn | string | Clé d'un autre champ : recharge les options quand sa valeur change — voir Listes en cascade ← v1.4.5 |
| dependsOnMode | 'server' \| 'client' | Défaut 'server' (re-fetch de url à chaque changement). 'client' : charge url une seule fois puis filtre localement ← v1.4.6 |
| dependsOnItemKey | string | Mode 'client' : propriété de chaque item API comparée à la valeur de dependsOn (défaut : la clé de dependsOn) ← v1.4.6 |
| dependsOnOffset | number | Mode 'client' : décalage numérique ajouté à la valeur de dependsOn avant comparaison ← v1.4.6 |
| remoteSearch | RemoteSearchConfig | Transforme un select en typeahead distant au lieu de charger url intégralement — voir Recherche distante (typeahead) — remoteSearch ← v1.7.5 |
| optionsFilter | FieldCondition \| FieldCondition[] | Ne garde que les items API dont une propriété correspond à une condition (ex: status === 'ACTIF') — voir Filtre des options API — optionsFilter ← v1.7.10 |
Note :
urletoptionssont mutuellement exclusifs. Siurlest défini,optionsest ignoré.Si la réponse n'est ni un tableau, ni un objet contenant un tableau à la clé attendue, les options restent vides et un avertissement est loggé dans la console (
[ngx-dynamic-form] ...).
Label composé — displayName concaténé
(v1.7.17) displayName peut combiner plusieurs propriétés de l'item API, y compris imbriquées, avec la syntaxe {{chemin}} (la même que l'interpolation de url) :
// Item API : { id: '37d2…', codeClient: 'CLI-PP-3F2247FB', telephone: '+221771234567',
// personnePhysique: { nom: 'Diop', prenom: 'Amadou' }, personneMorale: null }
displayName: 'codeClient' // → CLI-PP-3F2247FB (inchangé)
displayName: 'personnePhysique.nom' // → Diop
displayName: '{{codeClient}} - {{telephone}}' // → CLI-PP-3F2247FB - +221771234567
displayName: '{{personnePhysique.nom}} {{personnePhysique.prenom}}' // → Diop Amadou- Une valeur absente (
null/undefined) est remplacée par une chaîne vide et les espaces en trop sont réduits. On peut donc enchaîner des alternatives pour une liste mixte personnes physiques / morales :'{{codeClient}} - {{personnePhysique.nom}} {{personnePhysique.prenom}}{{personneMorale.raisonSociale}}'. - Fonctionne pour
select,multiselect,radio,dependsOnMode: 'client'etremoteSearch. displayValuereste une clé simple.
Listes en cascade — dependsOn
Nouveauté v1.4.5
Certains champs select/multiselect/radio doivent proposer des options différentes selon la valeur choisie dans un autre champ (ex : le champ « Rattaché à » d'un privilège ne doit proposer que les privilèges du niveau immédiatement inférieur au « Niveau » choisi). C'est le rôle de dependsOn : tant que le champ référencé n'a pas de valeur, les options restent vides ; à chaque changement de sa valeur, elles sont recalculées (statique) ou rechargées (url).
Options statiques dépendantes
Chaque option statique concernée déclare la valeur de dependsOn pour laquelle elle doit apparaître, via dependsOnValue :
{
key: 'niveau',
type: 'select',
label: 'Niveau',
col: 6,
options: [
{ label: '1', value: 1 },
{ label: '2', value: 2 },
{ label: '3', value: 3 },
],
validation: { required: true },
},
{
key: 'parent',
type: 'select',
label: 'Rattaché à',
col: 6,
dependsOn: 'niveau',
options: [
{ label: 'Tableau de bord', value: 10, dependsOnValue: 2 },
{ label: 'Rapports', value: 11, dependsOnValue: 2 },
{ label: 'Rapports mensuels', value: 20, dependsOnValue: 3 },
],
}Ici, choisir niveau = 2 ne propose que « Tableau de bord » et « Rapports » ; choisir niveau = 3 ne propose que « Rapports mensuels ».
Options distantes dépendantes
Pour des options chargées depuis une API, url peut référencer la valeur courante de n'importe quel champ du formulaire via {{cléDuChamp}} — la requête est automatiquement relancée à chaque changement du champ dependsOn :
{
key: 'niveau',
type: 'select',
label: 'Niveau',
col: 6,
options: [
{ label: '1', value: 1 }, { label: '2', value: 2 }, { label: '3', value: 3 }, { label: '4', value: 4 },
],
validation: { required: true },
},
{
key: 'parent',
type: 'select',
label: 'Rattaché à',
col: 6,
dependsOn: 'niveau',
url: '/api/privileges/select-options?niveau={{niveau}}', // → re-fetch à chaque changement de niveau
responseDataKey: 'data',
displayName: 'libelle',
displayValue: 'id',
}Note :
dependsOnne modifie pas automatiquement la valeur déjà sélectionnée du champ dépendant si elle devient invalide après un changement du champ parent — la validation du formulaire (required, etc.) s'en charge normalement à la soumission.
API distante qui ne filtre pas côté serveur — dependsOnMode: 'client'
Nouveauté v1.4.6
Certaines API ne proposent pas de paramètre de filtre — le pattern classique côté client est alors de charger toute la liste une seule fois, puis de la filtrer localement à chaque changement du champ parent (c'est exactement ce que fait un formulaire écrit à la main avec array.filter(...) dans un handler (selectionChange)). dependsOnMode: 'client' reproduit ce pattern :
{
key: 'niveau',
type: 'select',
label: 'Niveau',
col: 6,
options: [
{ label: '1', value: 1 }, { label: '2', value: 2 }, { label: '3', value: 3 }, { label: '4', value: 4 },
],
validation: { required: true },
},
{
key: 'parent',
type: 'select',
label: 'Rattaché à',
col: 6,
dependsOn: 'niveau',
dependsOnMode: 'client', // charge tous les privilèges une seule fois
url: '/tp-security-service/api/privileges?size=1000&page=0',
responseDataKey: 'data',
dependsOnItemKey: 'niveau', // compare item.niveau à …
dependsOnOffset: -1, // … niveau - 1 (le niveau immédiatement inférieur)
displayName: 'libelle',
displayValue: 'id',
}Équivalent à :
// Chargé une seule fois, indépendamment de `niveau` :
const privileges = await fetchAll('/tp-security-service/api/privileges?...');
// Recalculé à chaque changement de `niveau` :
const options = privileges.filter(p => p.niveau === niveau - 1);Sans dependsOnOffset, la comparaison est stricte (item[dependsOnItemKey] === valeurDeDependsOn) — utile quand l'item référence directement l'id du parent (ex : item.paysId === pays pour un couple pays/ville).
servervsclient: utilisezserver(par défaut) quand l'API accepte un paramètre de filtre — une seule requête pertinente par changement. Utilisezclientquand elle ne le fait pas — une seule requête au total, filtrage en mémoire ensuite (à réserver aux listes de taille raisonnable).
Recherche distante (typeahead) — remoteSearch
Nouveauté v1.7.5
Pour un champ select dont la liste d'options est trop grande pour être chargée entièrement (ex : recherche de client parmi plusieurs milliers), remoteSearch remplace le chargement eager de url par un typeahead : le rendu passe d'un mat-select classique à un input + mat-autocomplete, qui interroge url avec le texte tapé (debounce, requête annulée si l'utilisateur retape avant la réponse).
Interface RemoteSearchConfig
interface RemoteSearchConfig {
paramName?: string; // nom du query param recevant le texte tapé (défaut : 'q')
minChars?: number; // caractères minimum avant la première requête (défaut : 3)
debounceMs?: number; // délai après la dernière frappe (défaut : 300)
params?: Record<string, string | number>; // params statiques additionnels, ex: { size: 20 }
}Exemple
{
key: 'clientId',
type: 'select',
label: 'Client',
url: 'clients/recherche', // → GET clients/recherche?q=<saisie>&page=0&size=20
responseDataKey: 'content', // réponse Spring Page : le tableau est sous "content"
displayName: 'codeClient',
displayValue: 'id',
remoteSearch: {
minChars: 3,
params: { page: 0, size: 20 },
},
validation: { required: true },
}url,displayName,displayValueetresponseDataKeysont les mêmes propriétés que pour un select classique chargé via API —remoteSearchne fait que changer quand et commenturlest appelée, pas comment la réponse est interprétée. Une API qui répond{ data: [...] }ou un tableau brut fonctionne donc aussi bien qu'une réponse{ content: [...] }.- Si l'API attend un nom de param différent de
q(ex : recherche partelephoneou parnom), indiquez-le viaparamName. - Aucune requête n'est envoyée tant que le texte tapé est plus court que
minChars— évite les erreurs 400 des APIs qui imposent un minimum de caractères. - En cas d'échec de la requête (réseau, 4xx/5xx), le comportement est identique à un select classique : erreur loggée en console, liste d'options vide — l'utilisateur peut retaper pour relancer une recherche.
- Form Builder (v1.7.7) : une fois « Depuis une API » sélectionné pour un champ
select, une case « Recherche à distance » exposeparamName,minCharsetdebounceMs(paramsreste à définir en JSON brut pour l'instant, pas encore dans le Builder visuel). - Limites v1.7.5 :
multiselectavecremoteSearchn'est pas supporté (uniquementselect, un choix unique) ; en mode édition (valeur déjà présente dans le formulaire), le texte affiché initialement est la valeur brute du champ, faute d'un endpoint de résolution par id — pas de label pré-rempli tant que l'utilisateur n'a pas retapé une recherche.
Filtre des options API — optionsFilter
Nouveauté v1.7.10
Pour un champ select/multiselect/radio dont les options viennent d'une API (url), optionsFilter ne garde que les items dont une propriété correspond à une condition — avant leur mapping en { label, value }. Utile pour exclure des lignes désactivées/supprimées d'une API qui renvoie tout (pas de paramètre de filtre côté serveur, ou pas confirmé).
Interface
optionsFilter réutilise exactement la forme de FieldCondition (même 7 opérateurs que showWhen), avec field qui désigne ici une propriété brute de l'item API (ex: 'status'), pas la clé d'un autre champ du formulaire :
optionsFilter?: FieldCondition | FieldCondition[];
optionsFilterLogic?: 'AND' | 'OR'; // défaut 'AND' — indépendant de conditionLogic (showWhen)Exemple — ne garder que les pays actifs
{
key: 'paysId',
type: 'select',
label: 'Pays',
url: 'pays',
displayName: 'libelle',
displayValue: 'id',
optionsFilter: { field: 'status', operator: 'equals', value: 'ACTIF' },
}Avec une réponse API contenant des pays ACTIF, INACTIF et SUPPRIME, seuls les ACTIF apparaissent dans la liste déroulante.
Plusieurs conditions — ET / OU
{
key: 'paysId',
type: 'select',
label: 'Pays',
url: 'pays',
displayName: 'libelle',
displayValue: 'id',
optionsFilter: [
{ field: 'status', operator: 'equals', value: 'ACTIF' },
{ field: 'indicatifTel', operator: 'exists' },
],
optionsFilterLogic: 'AND', // les deux conditions doivent être vraies (défaut)
}- S'applique avant
dependsOnMode: 'client'— le filtrage parstatusréduit la liste mise en cache, quedependsOnaffine ensuite localement. - Ne s'applique qu'aux options chargées via
url— lesoptionsstatiques ne sont pas concernées (l'auteur du formulaire les contrôle déjà directement). - Form Builder (v1.7.10) : section « Filtrer les résultats » dans le panneau « Depuis une API », avec le même éditeur de conditions ET/OU que « Afficher quand » (sauf que le nom de propriété se saisit en texte libre, pas dans un menu déroulant de champs du formulaire).
Validation
interface FieldValidation {
required?: boolean;
email?: boolean;
minLength?: number;
maxLength?: number;
min?: number;
max?: number;
pattern?: string | RegExp;
custom?: (value: any) => string | null;
// Required conditionnel ← v1.6.0
requiredWhen?: FieldCondition | FieldCondition[]; // required activé si la condition est vraie
// Validation croisée ← v1.6.0
maxField?: string; // clé d'un autre champ — erreur si this.value > otherField.value
minField?: string; // clé d'un autre champ — erreur si this.value < otherField.value
messages?: {
required?: string;
email?: string;
minLength?: string;
maxLength?: string;
min?: string;
max?: string;
pattern?: string;
// Messages pour les nouvelles contraintes ← v1.6.0
requiredWhen?: string;
maxField?: string;
minField?: string;
// type: 'date' uniquement — affiché quand la date choisie est hors des bornes minDate/maxDate du champ ← v1.6.6
minDate?: string;
maxDate?: string;
};
}Exemples :
// Validateur avec message personnalisé
{
key: 'email',
type: 'email',
label: 'E-mail',
validation: {
required: true,
email: true,
messages: {
required: 'L\'e-mail est obligatoire.',
email: 'Format e-mail invalide.',
},
},
}
// Validateur custom
{
key: 'password',
type: 'password',
label: 'Mot de passe',
validation: {
required: true,
custom: (v) => {
if (!v || v.length < 8) return 'Minimum 8 caractères.';
if (!/[A-Z]/.test(v)) return 'Au moins une majuscule.';
return null;
},
},
}Validation par pattern — regex
La propriété validation.pattern accepte une chaîne de caractères (regex) ou un RegExp. Le Form Builder (v1.4.0) expose un champ de saisie pour la regex et trois presets d'un clic :
| Preset | Regex | Usage |
|---|---|---|
| Chiffres uniquement | ^\d+$ | Numéro de commande, code |
| Lettres uniquement | ^[a-zA-ZÀ-ÿ\s]+$ | Nom, prénom |
| Alphanumérique | ^[a-zA-Z0-9]+$ | Identifiant, code |
{
key: 'code',
type: 'text',
label: 'Code article',
validation: {
required: true,
pattern: '^[A-Z0-9]{4,8}$',
messages: { pattern: 'Le code doit comporter 4 à 8 caractères majuscules ou chiffres.' },
},
}Vérification d'unicité — uniqueCheck
Nouveauté v1.4.0
La propriété uniqueCheck ajoute un validateur asynchrone à un champ : à chaque saisie (avec un debounce de 400 ms), le plugin envoie un POST à l'URL configurée et affiche une erreur si la valeur existe déjà.
Configuration du champ
{
key: 'email',
type: 'email',
label: 'E-mail',
validation: { required: true, email: true },
uniqueCheck: {
url: 'https://api.monprojet.com/check/email',
message: 'Cet e-mail est déjà utilisé.',
},
}Contrat de l'API
Le plugin envoie :
POST /check/email
{ "value": "[email protected]" }L'API peut retourner :
- Un booléen brut :
true= valeur prise,false= disponible - Une enveloppe SUNTELECOMS :
{ "data": true }ou{ "data": false }
Les deux formats sont auto-détectés — aucune configuration supplémentaire.
UniquenessValidatorService
Le service est exporté et peut être utilisé directement dans n'importe quel composant :
import { inject } from '@angular/core';
import { UniquenessValidatorService } from '@suntelecoms/ngx-dynamic-form';
export class MonComposant {
private uniqueness = inject(UniquenessValidatorService);
myControl = new FormControl('', {
asyncValidators: [
this.uniqueness.build('https://api.monprojet.com/check/username', 'Nom d\'utilisateur déjà pris.'),
],
});
}Prérequis :
provideHttpClient()doit être présent dansapp.config.ts.
Champ téléphone international — phone
Nouveauté v1.4.0 · Sélecteur en menu avec recherche + apparence Material native (v1.4.7) · Drapeaux en SVG (v1.4.8)
Le type phone affiche un champ numérique masqué automatiquement selon le pays, précédé d'un sélecteur de pays (drapeau + indicatif). La valeur stockée dans le FormGroup est toujours au format E.164 (ex : +33612345678).
Deux rendus selon appearance :
outline/fill(par défaut) : un vrai<mat-form-field>— même apparence Material native que les autres champs. Le sélecteur de pays est un boutonmatPrefix(drapeau + indicatif) qui ouvre un menu avec un champ de recherche et la liste des 135 pays (drapeau, nom, indicatif) — cliquer un pays met à jour l'indicatif sans effacer le numéro déjà saisi. Les drapeaux sont des SVG (packageflag-icons), pas des emoji : sur Windows et dans certains webviews, les emoji drapeau (séquences de deux « regional indicator symbols ») retombent sur les deux lettres du code pays faute de police couleur adéquate — le SVG rend à l'identique partout.simple: la liste déroulante HTML native<select>d'origine, pour rester sans dépendance à Angular Material. Les<option>ne peuvent afficher que du texte (aucune image/CSS possible dedans), donc ce mode garde l'emoji — limitation du<select>natif, pas du champ.
Prérequis (apparence
outline/fill) : installerflag-iconset charger sa feuille de style globalement.npm install flag-iconsN'ajoutez pas directement
node_modules/flag-icons/css/flag-icons.min.cssdansangular.json: ce fichier référence deux dossiers de drapeaux (flags/4x3/xx.svgrectangulaire etflags/1x1/xx.svgcarré) qui partagent le même nom de fichier par pays. En configurationdevelopment(assets non hashés), Angular échoue avecTwo output files share the same path but have different contents. Ce champ n'utilise que la variante rectangulaire (.fiscarré jamais utilisé) — désactivez la variante carrée au niveau Sass à la place :// src/styles/vendor/flag-icons.scss @use "flag-icons/sass/flag-icons" with ( $flag-icons-use-square: false );// angular.json → projects.<app>.architect.build.options.styles "styles": ["src/styles/vendor/flag-icons.scss", "src/styles.css"]
Exemple de champ
{
key: 'mobile',
type: 'phone',
label: 'Téléphone mobile',
col: 6,
defaultCountry: 'SN', // Sénégal par défaut
validation: { required: true },
}Pays disponibles (135)
Tous les pays et territoires courants, classés par continent (Afrique, Amériques, Asie, Europe, Océanie), avec drapeau, indicatif et masque de saisie propre à chaque pays. Liste complète : PHONE_COUNTRIES dans projects/ngx-dynamic-form/src/lib/data/phone-countries.ts.
Interfaces et constantes exportées
import type { PhoneCountry } from '@suntelecoms/ngx-dynamic-form';
import { PHONE_COUNTRIES } from '@suntelecoms/ngx-dynamic-form';
// PhoneCountry
interface PhoneCountry {
code: string; // 'FR'
name: string; // 'France'
dialCode: string; // '+33'
flag: string; // '🇫🇷'
mask: string; // '# ## ## ## ##' (#=chiffre, autres chars=séparateurs)
}Affichage conditionnel — showWhen
Un champ peut être affiché ou masqué en fonction de la valeur d'un autre champ, sans écrire de code Angular.
// Condition unique — afficher "Entreprise" seulement si accountType === 'pro'
{
key: 'company',
type: 'text',
label: 'Entreprise',
showWhen: { field: 'accountType', operator: 'equals', value: 'pro' },
}
// Conditions multiples — logique AND (défaut)
{
key: 'discount',
type: 'number',
label: 'Remise (%)',
showWhen: [
{ field: 'accountType', operator: 'equals', value: 'pro' },
{ field: 'orderTotal', operator: 'greaterThan', value: 500 },
],
// conditionLogic: 'AND', // optionnel — AND est le défaut
}
// Conditions multiples — logique OU ← v1.4.0
{
key: 'promo',
type: 'text',
label: 'Code promo',
showWhen: [
{ field: 'accountType', operator: 'equals', value: 'pro' },
{ field: 'accountType', operator: 'equals', value: 'vip' },
],
conditionLogic: 'OR', // afficher si pro OU vip
}Opérateurs disponibles
| Opérateur | Description |
|---|---|
| equals | Valeur exactement égale |
| notEquals | Valeur différente |
| contains | Contient (string ou tableau) |
| greaterThan | Valeur numérique supérieure |
| lessThan | Valeur numérique inférieure |
| exists | Champ renseigné (non vide, non null) |
| notExists | Champ vide ou null |
Auto-remplissage depuis une API — lookup
Nouveauté v1.6.0
La propriété lookup déclenche un appel HTTP à la perte de focus du champ et remplit d'autres champs du formulaire avec les données de la réponse.
Interface FieldLookup
interface FieldLookup {
url: string; // Endpoint — supporte {{key}} pour interpoler la valeur du champ
method?: 'GET' | 'POST'; // défaut : 'GET'
populate: Record<string, string>;
// Clé = champ cible, Valeur = chemin dans la réponse (notation pointée)
// Ex : { 'nom': 'data.nom', 'typeCode': 'data.typeCode' }
triggerOn?: 'blur' | 'change'; // défaut : 'blur'
}Exemple — lookup souscripteur par téléphone
{
key: 'telephone',
type: 'phone',
label: 'Téléphone',
lookup: {
url: '/api/souscripteurs/check?telephone={{telephone}}',
method: 'GET',
populate: {
'prenom': 'data.prenom',
'nom': 'data.nom',
'email': 'data.email',
'typeCode':'data.typeCode',
},
},
}Quand l'utilisateur quitte le champ telephone, le plugin appelle GET /api/souscripteurs/check?telephone=<valeur> et utilise la réponse pour remplir automatiquement les champs prenom, nom, email et typeCode via patchValue.
Si l'API renvoie une erreur HTTP (4xx/5xx), le lookup est ignoré silencieusement — aucun champ n'est modifié.
Required conditionnel — requiredWhen
Nouveauté v1.6.0
requiredWhen active Validators.required uniquement si la condition est vraie — sans logique impérative dans le composant hôte.
{
key: 'raisonSociale',
type: 'text',
label: 'Raison sociale',
validation: {
requiredWhen: { field: 'typeCode', operator: 'equals', value: '3' },
messages: { requiredWhen: 'La raison sociale est obligatoire pour une personne morale.' },
},
}Le validateur est réévalué à chaque changement du formulaire — si la condition devient fausse, l'erreur disparaît.
Accepte aussi un tableau de conditions (avec conditionLogic: 'AND' | 'OR' hérité du champ) :
validation: {
requiredWhen: [
{ field: 'typeCode', operator: 'equals', value: '3' },
{ field: 'typeAssurance', operator: 'equals', value: 'PRO' },
],
}Validation croisée — maxField / minField
Nouveauté v1.6.0
Les propriétés maxField et minField sur FieldValidation créent un validateur de groupe qui compare deux champs numériques ou deux dates (voir Période de dates).
// Exemple : valeur vénale ne peut pas dépasser la valeur à neuf
{
key: 'valeurVenal',
type: 'number',
label: 'Valeur vénale (FCFA)',
validation: {
maxField: 'valeurNeuf',
messages: { maxField: 'La valeur vénale ne peut pas dépasser la valeur à neuf.' },
},
}| Propriété | Comportement |
|---|---|
| maxField: 'autreChamp' | Erreur si this.value > form.get('autreChamp').value |
| minField: 'autreChamp' | Erreur si this.value < form.get('autreChamp').value |
L'erreur est posée directement sur le FormControl concerné (pas sur le groupe parent), ce qui l'affiche sous le champ comme une erreur de validation normale.
Période de dates — date de début ≤ date de fin
(v1.7.18) maxField / minField fonctionnent aussi sur les types date, datetime-local, month, week et time. Les valeurs peuvent être des Date (sélection au calendrier) ou des chaînes ISO ('2026-06-01', valeurs pré-remplies depuis l'API en mode édition) ; la comparaison se fait au jour près pour une date.
{
key: 'dateEffet',
type: 'date',
label: "Date d'effet souhaitée",
validation: { required: true, maxField: 'dateFinValidite' },
},
{
key: 'dateFinValidite',
type: 'date',
label: 'Date de fin de validité',
validation: { required: true, minField: 'dateEffet' },
}- Calendrier : les dates hors période sont grisées — le calendrier de la date d'effet s'arrête à la date de fin choisie, celui de la date de fin commence à la date d'effet. Si
minDate/maxDateest aussi défini, la borne la plus stricte s'applique. - Message par défaut :
Doit être antérieure ou égale à « Date de fin de validité ».(oupostérieure ou égale à « … »pourminField) — personnalisable viamessages.maxField/messages.minField. - Déclarer les deux côtés (
maxFieldsur le début,minFieldsur la fin) affiche l'erreur sous le champ que l'utilisateur vient de modifier, quel qu'il soit. - En Stepper, les deux champs doivent être dans la même étape.
- Form Builder : section Validation d'un champ date → « Max basé sur champ » / « Min basé sur champ ».
Au moins un champ obligatoire — atLeastOneOf
Nouveauté v1.6.0
atLeastOneOf est un validateur de groupe qui exige qu'au moins un des champs listés soit renseigné. Disponible sur DynamicFormConfig (formulaire simple) et FormStep (par étape dans le stepper).
// Sur DynamicFormConfig
const config: DynamicFormConfig = {
atLeastOneOf: ['email', 'telephone'],
atLeastOneOfMessage: 'Renseignez au moins un moyen de contact.',
fields: [
{ key: 'email', type: 'email', label: 'E-mail' },
{ key: 'telephone', type: 'phone', label: 'Téléphone' },
],
};
// Sur FormStep
const step: FormStep = {
label: 'Contact',
atLeastOneOf: ['email', 'telephone'],
atLeastOneOfMessage: 'Renseignez au moins un moyen de contact.',
fields: [ /* ... */ ],
};L'erreur est affichée sous le formulaire/l'étape jusqu'à ce qu'au moins un champ ait une valeur non vide.
Guard de step — beforeNext
Nouveauté v1.6.0
beforeNext sur FormStep effectue un appel HTTP avant d'autoriser l'avancement vers l'étape suivante. Une réponse HTTP 4xx bloque la navigation et affiche le message d'erreur retourné par le serveur.
Interface StepGuard
interface StepGuard {
url: string; // Endpoint POST/GET appelé avant d'avancer
method?: 'GET' | 'POST'; // défaut : 'POST'
withFormValues?: boolean; // Envoie le formulaire complet de l'étape dans le corps (défaut : true)
resultKey?: string; // Clé dans la réponse à stocker dans stepResults (défaut : aucun)
errorMessage?: string; // Message d'erreur de fallback si le serveur n'en retourne pas
}Exemple — vérification d'immatriculation
const step: FormStep = {
label: 'Véhicule',
fields: [
{ key: 'immatriculation', type: 'text', label: 'Immatriculation' },
{ key: 'puissance', type: 'number', label: 'Puissance (CV)' },
],
beforeNext: {
url: '/api/souscriptions/check',
method: 'POST',
errorMessage: 'Vérification impossible, veuillez réessayer.',
},
};Le plugin envoie POST /api/souscriptions/check avec les valeurs de l'étape. Si le serveur répond HTTP 400 avec { "message": "Une souscription existe déjà…" }, ce message est affiché sous le stepper et la navigation est bloquée.
resultKey: si renseigné, la réponse HTTP 2xx est stockée dansstepResults[resultKey]accessible viagetStepForm()ou dansformSubmit. Utile pour propager un ID ou un résultat de calcul aux étapes suivantes.
API publique StepperFormComponent
@ViewChild('s') stepperRef!: StepperFormComponent;
// Accéder au FormGroup d'une étape spécifique
const step0Form = this.stepperRef.getStepForm(0);
console.log(step0Form.value);
// Mettre à jour des champs d'une étape
this.stepperRef.getStepForm(1).patchValue({ primeRC: 45000 });Icônes (Material Icons)
{ key: 'email', type: 'email', label: 'E-mail', icon: 'email', iconSuffix: 'verified' }| Champ | Icône recommandée |
|---|---|
| Nom, Prénom | person, badge |
| E-mail | email |
| Téléphone | phone |
| Mot de passe | lock |
| Adresse | home, location_on |
| Entreprise | business |
| Prix | euro, attach_money |
| Date | calendar_today |
| Pièce jointe | attach_file |
Disposition en grille (col)
La propriété col détermine le nombre de colonnes sur 12 occupées par le champ.
| col | Largeur | Usage |
|---|---|---|
| 12 | 100% | Pleine largeur (défaut) |
| 6 | 50% | Deux champs côte à côte |
| 4 | 33% | Trois champs côte à côte |
| 3 | 25% | Quatre champs côte à côte |
En dessous de 600 px, tous les champs passent automatiquement en pleine largeur.
Exemple avec sections :
fields: [
{ key: '_s1', type: 'heading', text: 'Informations', level: 3, col: 12 },
{ key: 'name', type: 'text', label: 'Nom', col: 6 },
{ key: 'email', type: 'email', label: 'E-mail', col: 6 },
{ key: '_div', type: 'divider', col: 12 },
{ key: '_s2', type: 'heading', text: 'Adresse', level: 3, col: 12 },
]API du composant DynamicForm
Inputs
| Propriété | Type | Description |
|---|---|---|
| [config] | DynamicFormConfig | Configuration du formulaire (obligatoire) |
| [initialValues] | Record<string, any> | Préremplissage des champs (mode édition) |
Outputs
| Événement | Type | Description |
|---|---|---|
| (formSubmit) | DynamicFormSubmitEvent | Formulaire soumis et valide |
| (formChange) | Record<string, any> | Émis à chaque modification |
| (formReset) | void | Formulaire réinitialisé |
| (fieldChange) | { key: string; value: any; form: Record<string, any> } | Émis à chaque changement de champ, avec la valeur et l'état complet du formulaire (v1.6.0) |
API publique via @ViewChild
@ViewChild('f') formRef!: DynamicFormComponent;
// Accéder au FormGroup Angular
const fg = this.formRef.getForm();
console.log(fg.value, fg.valid);
// Mettre à jour des valeurs sans réinitialiser
this.formRef.patchValues({ name: 'Nouveau nom' });<ngx-dynamic-form
#f
[config]="config"
[initialValues]="existingData"
(formSubmit)="onSubmit($event)"
(formChange)="onChange($event)"
/>Formulaires multi-étapes — Stepper
Nouveauté v1.2.0
Un formulaire stepper découpe la saisie en plusieurs étapes numérotées. L'utilisateur avance étape par étape — chaque étape est validée avant de passer à la suivante. À la fin, une étape Récapitulatif affiche toutes les valeurs saisies avec la possibilité de revenir corriger une étape.
Comment ça fonctionne
Étape 1 Étape 2 Étape 3 Récapitulatif
Identité ──► Adresse ──► Documents ──► Vérification + Envoi- Navigation linéaire : impossible de passer à l'étape suivante si l'étape en cours est invalide.
- Bouton Modifier dans le récapitulatif : permet de revenir à n'importe quelle étape.
- Un seul
formSubmit: les données de toutes les étapes sont fusionnées dans un seul objet au moment de l'envoi.
Utilisation du composant <ngx-stepper-form>
import { Component } from '@angular/core';
import { StepperFormComponent } from '@suntelecoms/ngx-dynamic-form';
import type { StepperFormConfig, DynamicFormSubmitEvent } from '@suntelecoms/ngx-dynamic-form';
@Component({
selector: 'app-inscription',
standalone: true,
imports: [StepperFormComponent],
template: `
<ngx-stepper-form [config]="config" (formSubmit)="onSubmit($event)" />
`,
})
export class InscriptionComponent {
config: StepperFormConfig = {
showSummary: true,
submitLabel: 'Valider',
steps: [
{
label: 'Identité',
icon: 'person',
fields: [
{ key: 'firstName', type: 'text', label: 'Prénom', col: 6, validation: { required: true } },
{ key: 'lastName', type: 'text', label: 'Nom', col: 6, validation: { required: true } },
{ key: 'email', type: 'email', label: 'E-mail', col: 12, validation: { required: true, email: true } },
],
},
{
label: 'Adresse',
icon: 'home',
fields: [
{ key: 'street', type: 'text', label: 'Rue', col: 12, validation: { required: true } },
{ key: 'city', type: 'text', label: 'Ville', col: 6, validation: { required: true } },
{ key: 'zip', type: 'text', label: 'Code postal', col: 6, validation: { required: true } },
],
},
{
label: 'Confirmation',
icon: 'check_circle',
fields: [
{ key: 'terms', type: 'checkbox', placeholder: "J'accepte les CGU", col: 12,
validation: { required: true } },
],
},
],
};
onSubmit(event: DynamicFormSubmitEvent): void {
console.log(event.value);
}
}Configuration — StepperFormConfig
interface StepperFormConfig {
steps: FormStep[]; // Liste des étapes (obligatoire)
submitLabel?: string; // Libellé du bouton Envoyer (défaut : "Envoyer")
showReset?: boolean; // Afficher Réinitialiser (défaut : false)
resetLabel?: string; // Libellé du bouton Reset
linear?: boolean; // Bloque la navigation si l'étape est invalide (défaut : true)
orientation?: 'horizontal' | 'vertical';
showSummary?: boolean; // Ajoute une étape Récapitulatif à la fin (défaut : false)
summaryLabel?: string; // Libellé de l'étape récapitulatif (défaut : "Récapitulatif")
cssClass?: string;
debug?: boolean;
}Configuration — FormStep
interface FormStep {
label: string; // Titre de l'étape affiché dans la barre de navigation
icon?: string; // Icône Material Icons (optionnelle)
description?: string; // Sous-titre de l'étape (optionnel)
fields: DynamicFormField[];
// ← v1.6.0
beforeNext?: StepGuard; // Guard HTTP exécuté avant l'avancement vers l'étape suivante
atLeastOneOf?: string[]; // Au moins un des champs listés doit être renseigné (étape)
atLeastOneOfMessage?: string; // Message d'erreur personnalisé
}L'étape Récapitulatif
Quand showSummary: true, une étape supplémentaire est ajoutée automatiquement à la fin du stepper. Elle affiche :
- Toutes les étapes sous forme de sections distinctes
