Si vous êtes habitué à travailler avec les formulaires réactifs (Reactive forms) et à créer de nombreux formulaires, vous avez probablement remarqué que cela prend du temps et que vous répétez souvent les mêmes étapes. Aujourd'hui, après avoir testé et exploré plusieurs façons de simplifier cela, j'ai décidé de créer ma propre version de formulaire dynamique. Bien sûr, il existe déjà des solutions existantes, mais j'ai choisi de m'appuyer sur certains concepts pour créer mon propre composant dynamique.

Code source disponible sur gitLab https://gitlab.com/eroamba/dynamic_form


Introduction

Notre formulaire est dynamique et entièrement réutilisable à 100 %. Il est simple à configurer et offre une grande flexibilité. Dans un prochain article, nous explorerons une version améliorée de ce formulaire dynamique.

Prérequis

Nous utiliserons la version @angular/cli@16.1.0 pour notre cas pratique. Avant de commencer, assurez-vous d'ajouter Bootstrap à votre projet en ajoutant les liens suivants dans le fichier index.html :

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>

Ici Bootstrap est utilisé pour gérer le positionnement des éléments du formulaire avec les classes CSS.

Création du projet

Commencez par générer un nouveau projet Angular en utilisant la commande suivante :

ng new dynamic_form

Création du composant dynamique

Nous allons créer un composant qui contiendra tous nos champs de formulaire désirés. Dans notre cas, nous incluons la plupart des types de champs courants tels que le texte, l'e-mail, la date, le nombre, les cases à cocher, les sélecteurs et les zones de texte. Vous pouvez ajouter d'autres types selon vos besoins.

Commencez par générer le composant dynamic-input avec la commande suivante :

ng generate component dynamic-input

Ensuite, dans le fichier dynamic-input.component.ts, importez les modules nécessaires et définissez les propriétés et méthodes nécessaires :

import { CommonModule } from '@angular/common';
import { Component, Input, Optional, Self } from '@angular/core';
import { ControlContainer, ControlValueAccessor, FormGroup, FormGroupDirective, FormsModule, NgControl, ReactiveFormsModule } from '@angular/forms';

export const NOOP_VALUE_ACCESSOR: ControlValueAccessor =
{
  writeValue(): void { },
  registerOnChange(): void { },
  registerOnTouched(): void { }
};

@Component({
  selector: 'app-dynamic-input',
  standalone: true,
  templateUrl: './dynamic-input.component.html',
  styleUrls: ['./dynamic-input.component.css'],
  imports: [CommonModule, FormsModule, ReactiveFormsModule], 
  viewProviders: [{
    provide: ControlContainer,
    useExisting: FormGroupDirective
  }]
})
export class DynamicInputComponent {
  @Input() checkedValue: boolean = true
  @Input() data: any[]=[]
  @Input() dynamicForm: FormGroup=new FormGroup({});


  constructor(@Self() @Optional() public ngControl: NgControl) {
    if (this.ngControl) {
      this.ngControl.valueAccessor = NOOP_VALUE_ACCESSOR;
    }
  }



  getErrorMessage(control: { name: any; validations: any; }) {
    const formControl = this.dynamicForm.get(control.name);

    if (formControl) { 
      for (let validation of control.validations) {
        if (formControl.hasError(validation.name)) {
          return validation.message;
        }
      }
    }
  
    return '';
  }

}


Cela permettra au composant dynamic-input d'accéder au FormGroup parent.

viewProviders: [{ provide: ControlContainer, useExisting: FormGroupDirective }]

Dans notre composant dynamic-input, le fichier HTML est crucial pour afficher dynamiquement les différents types de champs de formulaire en fonction des données fournies. Jetons un coup d'œil au code HTML du composant :

<div class="row">
    <ng-container *ngFor="let item of data" [ngSwitch]="item.type">
  
      <div *ngSwitchCase="'text'" [class]="item.classContainer">
        <label *ngIf="item.label" [for]="item.id">{{item.label}}</label>
  
        <div class="form-group mb-3">
          <input [type]="item.type" [id]="item.id" [formControlName]="item.formControlName" class="form-control"
            [placeholder]="item.placeholder" />
          <span *ngIf="dynamicForm.get(item.formControlName)?.invalid && dynamicForm.get(item.formControlName)?.touched">
            {{ getErrorMessage(item) }}
          </span>
        </div>
      </div>
  
      <div *ngSwitchCase="'email'" [class]="item.classContainer">
        <label *ngIf="item.label" [for]="item.id">{{item.label}}</label>
  
        <div class="form-group mb-3">
          <input [type]="item.type" [id]="item.id" [formControlName]="item.formControlName" class="form-control"
            [placeholder]="item.placeholder" />
          <span *ngIf="dynamicForm.get(item.formControlName)?.invalid && dynamicForm.get(item.formControlName)?.touched">
            {{ getErrorMessage(item) }}
          </span>
        </div>
      </div>
  
      <div *ngSwitchCase="'date'" [class]="item.classContainer">
        <label *ngIf="item.label" [for]="item.id">{{item.label}}</label>
    
        <div class="form-group mb-3">
            <input [type]="item.type" [id]="item.id" [formControlName]="item.formControlName" class="form-control"
                [placeholder]="item.placeholder" />
            <span *ngIf="dynamicForm.get(item.formControlName)?.invalid && dynamicForm.get(item.formControlName)?.touched">
                {{ getErrorMessage(item) }}
            </span>
        </div>
    </div>
    
  
      <div *ngSwitchCase="'select'" [class]="item.classContainer">
        <div class="form-group">
          <label [for]="item.id">{{item.label}}</label>
  
          <select *ngIf="item.isMultiple && !item.isKeyValue" class="form-control" [id]="item.id" [formControlName]="item.formControlName"
            multiple>
            <option value="" disabled selected>{{item.placeholder}}</option> 
            <option *ngFor="let option of item.options" [value]="option">{{option}}</option>
          </select>
          <select *ngIf="!item.isMultiple && !item.isKeyValue" class="form-control" [id]="item.id" [formControlName]="item.formControlName">
            <option value="" disabled selected>{{item.placeholder}}</option> 
            <option *ngFor="let option of item.options" [value]="option">{{option}}</option>
          </select>
  
          <select *ngIf="!item.isMultiple && item.isKeyValue && item.isKeyValue==true" class="form-control" [id]="item.id" [formControlName]="item.formControlName">
            <option value="" disabled selected>{{item.placeholder}}</option> 
            <option *ngFor="let option of item.options" [value]="option.id">{{option.name}}</option>
          </select>
  
        </div>
      </div>
  
      <div *ngSwitchCase="'number'" [class]="item.classContainer">
        <label *ngIf="item.label" [for]="item.id">{{item.label}}</label>
  
        <div class="form-group mb-3">
          <input [type]="item.type" [id]="item.id" [formControlName]="item.formControlName" class="form-control"
            [placeholder]="item.placeholder" />
          <span *ngIf="dynamicForm.get(item.formControlName)?.invalid && dynamicForm.get(item.formControlName)?.touched">
            {{ getErrorMessage(item) }}
          </span>
        </div>
      </div>
  
      <div *ngSwitchCase="'radio'">
        <label class="pl-3" *ngIf="item.label" [for]="item.id">{{ item.label }}</label>
  
        <div [class]="item.classContainer" style="align-items: center;">
          <div class="custom-control custom-radio mr-4">
            <input id="false" type="radio" class="custom-control-input " [value]="false" [name]="item.formControlName"
              [formControlName]="item.formControlName">
            <label class="custom-control-label" for="true">{{item.labelRadio1}}</label>
          </div>
  
          <div class="custom-control custom-radio">
            <input id="true" type="radio" class="custom-control-input" [value]="true" [name]="item.formControlName"
              [formControlName]="item.formControlName">
            <label class="custom-control-label" for="false">{{item.labelRadio2}}</label>
          </div>
        </div>
      </div>
  
      <div *ngSwitchCase="'textarea'" [class]="item.classContainer">
        <label *ngIf="item.label" [for]="item.id">{{item.label}}</label>
        <div class="form-group mb-3">
          <textarea [id]="item.id" [formControlName]="item.formControlName" class="form-control" id="description"
            [placeholder]="item.placeholder" rows="3"></textarea>
          <span *ngIf="dynamicForm.get(item.formControlName)?.invalid && dynamicForm.get(item.formControlName)?.touched">
            {{ getErrorMessage(item) }}
          </span>
        </div>
      </div>
    </ng-container>
  </div>
  
  

Ce code génère dynamiquement les champs de formulaire en fonction des données fournies dans data. Chaque élément de data est passé à travers une boucle ngFor, et en fonction du type de champ de formulaire spécifié dans item.type, le composant affiche le champ de formulaire correspondant. Cette approche nous permet d'avoir un composant de formulaire flexible et réutilisable pour une variété de cas d'utilisation.

Après avoir créé notre composant dynamic-input, nous devons l'importer dans notre module principal AppModule pour pouvoir l'utiliser dans notre application. Voici comment vous pouvez le faire :

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    DynamicInputComponent,
    ReactiveFormsModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})

DynamicInputComponent est un standalone component c’est pour celà nous l’avons mis dans imports et non dans déclarations.

Pour organiser notre projet de manière propre et maintenable, nous allons créer un dossier form-data où nous stockerons nos fichiers de formulaire au format JSON. Dans ce dossier, nous aurons un fichier nommé role.ts qui contiendra notre formulaire de rôle. Voici à quoi cela pourrait ressembler :

export const ROLE_FORM=[
    {
      type: "text",
      id: "name",
      label: "nom",
      placeholder: "Saisissez le nom",
      formControlName: "name",
      classContainer: "col-sm-12",
      defaultValue:"",
      validations: [
        {
          name: "required",
          validator: "required",
          message: "Name is required"
        }
      ]
    },
    {
      type: "textarea",
      id: "description",
      label: "Description",
      placeholder: "Saisissez la description",
      formControlName: "description",
      classContainer: "col-sm-12",
      defaultValue:"",
      validations: [
        {
          name: "required",
          validator: "required",
          message: "Description is required"
        }
      ]
    },
  ];
  
Generate form with json angular

Aussi pour member.ts

export const MEMBER_FORM = [
  {
    "type": "select",
    "id": "civilite",
    "label": "Civilité",
    "formControlName": "civilite",
    "classContainer": "col-4",
    "isKeyValue": true,
    "options": [
      { "name": "Monsieur", "id": "monsieur" },
      { "name": "Madame", "id": "madame" },
      { "name": "Mademoiselle", "id": "mademoiselle" }
    ],
    "placeholder": "Veuillez sélectionner la civilité",
    "defaultValue": "",
    "validations": []
  },

  {
    "type": "text",
    "id": "last_name",
    "label": "Nom de famille",
    "placeholder": "Entrez le nom de famille",
    "formControlName": "last_name",
    "classContainer": "col-sm-4",
    "defaultValue": "",
    "validations": [
      {
        "name": "required",
        "validator": "required",
        "message": "Le nom de famille est requis"
      }
    ]
  },
  {
    "type": "text",
    "id": "first_name",
    "label": "Prénoms",
    "placeholder": "Entrez les prénoms",
    "formControlName": "first_name",
    "classContainer": "col-sm-4",
    "defaultValue": "",
    "validations": [
      {
        "name": "required",
        "validator": "required",
        "message": "Le prénom est requis"
      }
    ]
  },
  {
    "type": "email",
    "id": "email",
    "label": "Email",
    "placeholder": "Entrez l'email",
    "formControlName": "email",
    "classContainer": "col-sm-6",
    "defaultValue": "",
    "validations": [
      {
        "name": "required",
        "validator": "required",
        "message": "L'email est requis"
      },
      {
        "name": "email",
        "validator": "email",
        "message": "L'email est invalide"
      }
    ]
  },
  {
    "type": "text",
    "id": "phone",
    "label": "Téléphone",
    "placeholder": "Entrez le numéro de téléphone",
    "formControlName": "phone",
    "classContainer": "col-sm-6",
    "defaultValue": "",
    "validations": [
      {
        "name": "required",
        "validator": "required",
        "message": "Le numéro de téléphone est requis"
      }
    ]
  },
  {
    "type": "select",
    "id": "status",
    "label": "Statut",
    "formControlName": "status",
    "classContainer": "col-6",
    "isKeyValue": true,
    "options": [
      { "name": "Actif", "id": "actif" },
      { "name": "Inactif", "id": "inactif" }
    ],
    "placeholder": "Veuillez sélectionner le statut",
    "defaultValue": "",
    "validations": []
  },

  {
    "type": "select",
    "id": "user_type",
    "label": "Type d'utilisateur",
    "formControlName": "user_type",
    "classContainer": "col-6",
    "isKeyValue": true,
    "options": [
      { "name": "Utilisateur régulier", "id": "regular" },
      { "name": "Administrateur", "id": "admin" }
    ],
    "placeholder": "Veuillez sélectionner le type d'utilisateur",
    "defaultValue": "",
    "validations": []
  }
]

Dynamic form angular with json file

Pour terminer, voyons comment intégrer notre composant DynamicInputComponent dans notre composant principal de l'application.

Dans le composant principal (app.component.html), nous utilisons le DynamicInputComponent pour créer des formulaires dynamiques pour deux types d'utilisateurs : "Type utilisateur" et "Membre". Nous organisons ces deux types d'utilisateurs dans des onglets utilisant la bibliothèque Bootstrap.

<div class="container">
  <div class="row">
    <h1>Form dynamic Angular V1</h1>
  </div>
  <div class="row">
    <div class="col-md-10">
      <ul class="nav nav-tabs" id="myTab" role="tablist">
        <li class="nav-item" role="presentation">
          <button
            class="nav-link active"
            id="home-tab"
            data-bs-toggle="tab"
            data-bs-target="#home-tab-pane"
            type="button"
            role="tab"
            aria-controls="home-tab-pane"
            aria-selected="true"
          >
            Type utilisateur
          </button>
        </li>
        <li class="nav-item" role="presentation">
          <button
            class="nav-link"
            id="profile-tab"
            data-bs-toggle="tab"
            data-bs-target="#profile-tab-pane"
            type="button"
            role="tab"
            aria-controls="profile-tab-pane"
            aria-selected="false"
          >
            Membre
          </button>
        </li>
      </ul>
      <div class="tab-content" id="myTabContent">
        <div
          class="tab-pane fade show active"
          id="home-tab-pane"
          role="tabpanel"
          aria-labelledby="home-tab"
          tabindex="0"
        >
          <form class="p-4" [formGroup]="fbGroupRole">
            <app-dynamic-input
              [data]="formDataRole"
              [dynamicForm]="fbGroupRole"
            ></app-dynamic-input>
            <div class="d-flex justify-content-end pt-4">
              <button class="btn btn-primary" (click)="saveRole()">Enregistrer Rôle</button>
            </div>
          </form>
        </div>
        <div
          class="tab-pane fade"
          id="profile-tab-pane"
          role="tabpanel"
          aria-labelledby="profile-tab"
          tabindex="0"
        >
          <form class="p-4" [formGroup]="fbGroupMember">
            <app-dynamic-input
              [data]="formDataMember"
              [dynamicForm]="fbGroupMember"
            ></app-dynamic-input>
            <div class="d-flex justify-content-end pt-4">
              <button class="btn btn-primary" (click)="saveMember()">Enregistrer Membre</button>

            </div>
          </form>
        </div>
      </div>
    </div>
  </div>
</div>

Dynamic form angular

la partie ts

import { Component } from '@angular/core';
import { ROLE_FORM } from './form-data/role';
import { MEMBER_FORM } from './form-data/member';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'dynamic_form';
  formDataRole: any[] = ROLE_FORM;
  formDataMember: any[] = MEMBER_FORM;

  fbGroupRole:FormGroup=new FormGroup({});
  fbGroupMember:FormGroup=new FormGroup({});


  
  constructor(private fb: FormBuilder) {
    this.fbGroupRole= this.fb.group(this.createRoleFormControls());
    this.fbGroupMember= this.fb.group(this.createMemberFormControls());
  }



  createRoleFormControls() {
    const formControls: any = {};

    this.formDataRole.forEach(control => {
      let validators: any[] = [];

      if (control.validations) {
        control.validations.forEach((validation:any )=> {
          if (validation.validator === 'required') {
            validators.push(Validators.required);
          } else if (validation.validator === 'email') {
            validators.push(Validators.email);
          }
        });
      }

      // Définition de la valeur par défaut du contrôle
      const defaultValue = control.defaultValue !== undefined ? control.defaultValue : '';

      // Création du contrôle avec sa valeur par défaut et ses validateurs
      formControls[control.formControlName] = [
        defaultValue,
        validators
      ];
    
    });

    return formControls;
  }

  createMemberFormControls() {
    const formControls: any = {};

    this.formDataMember.forEach(control => {
      let validators: any[] = [];

      if (control.validations) {
        control.validations.forEach((validation:any )=> {
          if (validation.validator === 'required') {
            validators.push(Validators.required);
          } else if (validation.validator === 'email') {
            validators.push(Validators.email);
          }
        });
      }

      // Définition de la valeur par défaut du contrôle
      const defaultValue = control.defaultValue !== undefined ? control.defaultValue : '';

      // Création du contrôle avec sa valeur par défaut et ses validateurs
      formControls[control.formControlName] = [
        defaultValue,
        validators
      ];
    
    });

    return formControls;
  }

  saveRole() {
    // Afficher les valeurs du formulaire de rôle
    alert(JSON.stringify(this.fbGroupRole.value));
  }

  saveMember() {
    // Afficher les valeurs du formulaire de membre
    alert(JSON.stringify(this.fbGroupMember.value));
  }


}

Dynamic form with angular

En conclusion, notre composant de formulaire dynamique fonctionne parfaitement, et nous avons la possibilité de personnaliser son apparence en utilisant du CSS pour positionner les champs selon nos besoins. Cette approche nous permet de créer des formulaires dynamiques qui s'adaptent à différents scénarios d'utilisation.

Cependant, malgré sa flexibilité, je trouve que cette approche a ses limites. Dans notre prochain article, nous explorerons une version améliorée de notre composant dynamique, visant à le rendre encore plus flexible et adaptable à une plus grande variété de cas d'utilisation.

Restez à l'écoute pour la prochaine version de notre composant dynamique de formulaire, et n'hésitez pas à expérimenter et à personnaliser celui-ci selon vos besoins spécifiques

#angular Angular #ngConf ng-conf Deborah Kurata John Papa

Code source disponible sur gitLab https://gitlab.com/eroamba/dynamic_form