Template Driven Forms avec angular 9

10/02/20 danny

Les formulaires sont des éléments essentiels d'une Application Web.

Angular nous offre plusieurs moyens de les gèrer .

Dans ce tutoriel nous utiliserons le moyen le plus simple les Template Driven Forms.

Nous créerons une série de Formulaires (Forms) à partir de rien (from scratch).


Ce que nous allons faire

  • Avant de Commencer
    Qu'est ce qu'un Formulaire, comment Angular gère les Forms.
    Template Driven Forms ou Reactive form ?
     
  • Création de notre projet Angular
    Nous utiliserons un projet existant contenant les fonctionnalités essentielles.
    Le projet a été généré avec Angular CLI.
    Il utilise le Routing et le Lazy Loading.
    Il intègre le Framework CSS Bootstrap.
     
  • Prototype bootstrap
    Comment créer des formulaires avec bootstrap.
     
  • Intégration dans notre projet angular
    Comment Intégrer nos formulaires dans notre projet
     
  • Effectuer les Tests
    Unitaires et end to end.
     
  • Code source
    Le code complet du projet sur github.

Qu'est ce qu'un formulaire

Les Formulaires sont des éléments incontournables d'une application Web.
Ils permettent de visualiser toute sorte de données.
Ils permettent de les créer, de les modifier ou de les supprimer.

Ils sont facilement reconnaissables et les utilisateurs sont totalement habitués à les utiliser.

Si vous regardez l'exemple suivant vous comprenez très vite qu'il s'agit d'un formulaire de film.
Il contient des informations comme son nom, sa date de sortie ou ses résultats au box-office.
 

De surcroît il dispose de plusieurs boutons (create, Update ou Delete) qui permettront d'effectuer des actions précises.
Et les plus habitués reconnaitront évidemment le style visuel du célèbre Framework CSS bootstrap.

Dans notre tutoriel nous allons voir comment intégrer ce type de formulaire dans une application angular.

Le développement informatique étant à forte connotation anglo saxonne, les exemples utiliseront la langue de shakespeare.
Le cinéma étant un domaine plus que connu les exemples seront simplement basés sur des films de la pop culture.


Angular et les Forms

La documentation Angular concernant les forms est accessible ici
https://angular.io/guide/forms

Nous essaierons de suivre les conseils qui y sont prodigués en respectant les Best Practices préconisées par Angular.

Angular gère les forms de deux manières.

  • Template Driven Forms
  • Reactive Forms

On pourrait diviser le développement d'une Application Web en 2 parties.

  • La partie graphique
    L'interface utilisateur ou UI
    C'est à dire tout ce que nous voyons à l'écran.
     
  • La partie logique
    Que l'on pourrait aussi appelée partie Métier ou Partie traitement.
    En faisant simple comment tout ceci fontionne.



De façon assez sommaire on pourrait résumer le fonctionnement des forms sous angular ainsi

  • Template Driven Forms
    La partie graphique et la partie logique sont traitées au même endroit au niveau du template (les fichiers html).
    Le développement est simple mais s'avère peu performant sur des projets complexes.
     
  • Reactive Forms
    La partie graphique et la partie logique sont séparées très distinctement.
    Partie grapique dans des fichiers HTML et partie logique dans les fichiers Typescript.
    Cette méthode est plus difficile à mettre en place mais plus efficace sur des projets complexes.


Dans ce tutoriel nous allons traiter le cas des Template Driven Forms.
Traduisons en français tout d'abord ces 3 mots

  • Template : modèle
  • Driven : conduit , dirigé
  • Form : formulaire
     

En traduisant mot à mot cela pourrait être Gestion des Formulaires via les templates.


Création du projet Angular

Pour pouvoir continuer ce tutoriel nous devons bien evidemment disposer de certains éléments

  • Node.js : La plateforme javascript
  • Git : Le logiciel de gestion de versions. 
  • Angular CLI : L'outil fourni par Angular.
  • Visual Studio code : Un éditeur de code.

Vous pouvez consulter le tutoriel suivant qui vous explique en détails comment faire


Nous allons utiliser un projet existant dont les caractéristiques sont

  • Genéré avec Angular CLI
  • Gestion du Routing
  • Gestion du Lazy loading
  • Utilisation du Framework CSS Bootstrap
# Créez un répertoire demo (le nom est ici arbitraire)
mkdir demo

# Allez dans ce répertoire
cd demo

# Récupérez le code source sur votre poste de travail
git clone https://github.com/ganatan/angular-example-bootstrap.git

# Allez dans le répertoire qui a été créé
cd angular-example-bootstrap

# Exécutez l'installation des dépendances (ou librairies)
npm install

# Exécutez le programme
npm run start

# Vérifiez son fonctionnement en lançant dans votre navigateur la commande
http://localhost:4200/

Notre prototype contient la partie graphique finale.
Pour avoir plus de détails vous pouvez consulter le tutoriel sur Angular et Bootstrap

Prototype avec Bootstrap et Angular


Prototype Bootstrap

Pour créer notre prototype nous utiliserons un exemple fourni par Bootstrap.
L'exemple est ici
https://getbootstrap.com/docs/4.4/components/forms/#form-controls

Vous retrouverez cet exemple dans le répertoire UI de notre code source.
Il s'agit du fichier template-driven-forms.html.

Je vous donne ci-dessous les éléments spécifique à l'affichage du formulaire.

ui/template-driven-forms.html
    <main>
      <div class="container-fluid">
        <div class="row justify-content-center pt-2">
          <div class="text-center col">
            <h1 class="h3">Feature : Template-driven Forms</h1>
            <hr>
          </div>
        </div>
        <div class="row justify-content-center p-2">
          <div class="col-12 col-sm-12 col-md-6 col-lg-6 col-xl-6">
            <div class="card">
              <div class="card-header text-center">
                <h4>Movie : <span class="text-primary font-weight-bold">Avengers: Endgame</span></h4>
              </div>
              <div class="card-body">
                <form>
                  <div class="form-row">
                    <div class="form-group col-md-6">
                      <label for="inputEmail4">Name</label>
                      <input type="text" class="form-control" id="inputEmail4" value="Avengers: Endgame">
                    </div>
                    <div class="form-group col-md-6">
                      <label for="inputPassword4">Release Date</label>
                      <input type="text" class="form-control" id="inputPassword4" value="April 24, 2019">
                    </div>
                  </div>
                  <div class="form-row">
                    <div class="form-group col-md-4">
                      <label for="inputAddress">Domestic</label>
                      <input type="text" class="form-control" id="inputAddress" placeholder="" value="$858,373,000">
                    </div>
                    <div class="form-group col-md-4">
                      <label for="inputAddress">International</label>
                      <input type="text" class="form-control" id="inputAddress" placeholder="" value="$1,939,427,564">
                    </div>
                    <div class="form-group col-md-4">
                      <label for="inputAddress2">Worldwide</label>
                      <input type="text" class="form-control" id="inputAddress2" placeholder="" value="$2,797,800,564">
                    </div>
                  </div>
                  <div class="form-row justify-content-center">
                    <button type="submit" class="btn btn-primary mr-4">Create</button>
                    <button type="submit" class="btn btn-primary mr-4">Update</button>
                    <button type="submit" class="btn btn-primary">Delete</button>
                  </div>
                </form>
              </div>
            </div>
          </div>
        </div>
      </div>
    </main>

Nouvelles fonctionnalités

Nous allons rajouter une page dédiée au Template Driven Form dans notre projet.
C'est dans cette page que nous ferons tous nos tests et modifications.

Tout d'abord créons la nouvelle page avec les commandes Angular CLI correspondantes.

# Création de la page dédiée au Template Driven Forms
ng generate component modules/application/template-driven-forms  --module=app
ng generate module modules/application/template-driven-forms --routing  --module=app
src/app/app-routing.module.ts
  {
    path: 'template-driven-forms',
    loadChildren: () => import('./modules/application/template-driven-forms/template-driven-forms.module')
      .then(mod => mod.TemplateDrivenFormsModule)
  },
src/app/modules/application/template-driven-forms/template-driven-forms-routing.module.ts
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

import { TemplateDrivenFormsComponent } from './template-driven-forms.component';

const routes: Routes = [
  { path: '', component: TemplateDrivenFormsComponent },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class TemplateDrivenFormsRoutingModule { }
src/app/modules/application/template-driven-forms/template-driven-forms.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { TemplateDrivenFormsComponent } from './template-driven-forms.component';
import { TemplateDrivenFormsRoutingModule } from './template-driven-forms-routing.module';

@NgModule({
  imports: [
    CommonModule,
    TemplateDrivenFormsRoutingModule
  ],
  exports: [
    TemplateDrivenFormsComponent
  ],
  declarations: [
    TemplateDrivenFormsComponent
  ],
  providers: [
  ],
})
export class TemplateDrivenFormsModule { }
src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { HomeComponent } from './modules/general/home/home.component';
import { NotFoundComponent } from './modules/general/not-found/not-found.component';
import { AppRoutingModule } from './app-routing.module';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
    NotFoundComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }
src/modules/general/home/home.component.ts
        {
          name: 'Template Driven Forms',
          description: 'Build an Angular form with a component and template',
          icon: 'far fa-file-alt',
          link: 'template-driven-forms'
        },

Integration dans notre projet

Nous allons utiliser la directive ngModel.
Pour cela importons FormsModule dans la liste des modules.

  • template-driven-forms.module.ts


Ensuite copions le prototype bootstrap dans notre projet.

Nous allons intégrer la partie graphique.
Le CSS et le HTML seront indiqués dans les fichiers suivants.

  • template-driven-forms.component.css
  • template-driven-forms.component.html
src/app/modules/application/template-driven-forms/template-driven-forms.module.ts
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { TemplateDrivenFormsComponent } from './template-driven-forms.component';
import { TemplateDrivenFormsRoutingModule } from './template-driven-forms-routing.module';
import { FormsModule } from '@angular/forms';

@NgModule({
  imports: [
    CommonModule,
    TemplateDrivenFormsRoutingModule,
    FormsModule
  ],
  exports: [
    TemplateDrivenFormsComponent
  ],
  declarations: [
    TemplateDrivenFormsComponent
  ],
  providers: [
  ],
})
export class TemplateDrivenFormsModule { }
src/app/modules/application/template-driven-forms/template-driven-forms.css
.card {
	display: block;
	background-color: rgba(255, 255, 255, .8);
	box-shadow: 0 1px 3px rgba(0, 0, 0, .12), 0 1px 2px rgba(0, 0, 0, .24);
	border-radius: 2px;
	transition: all .2s ease-in-out;
	cursor: pointer;
}

.card:hover {
	box-shadow: 0 10px 20px rgba(0, 0, 0, .19), 0 6px 6px rgba(0, 0, 0, .23);
}
src/app/modules/application/template-driven-forms/template-driven-forms.html
<div class="row justify-content-center pt-2">
  <div class="text-center col">
    <h1 class="h3">Feature : Template-driven Forms</h1>
    <hr>
  </div>
</div>
<div class="row justify-content-center p-2">
  <div class="col-12 col-sm-12 col-md-6 col-lg-6 col-xl-6">
    <div class="card">
      <div class="card-header text-center">
        <h4>Movie : <span class="text-primary font-weight-bold">Avengers: Endgame</span></h4>
      </div>
      <div class="card-body">
        <form>
          <div class="form-row">
            <div class="form-group col-md-6">
              <label for="name">Name</label>
              <input type="text" class="form-control" id="name" value="Avengers: Endgame">
            </div>
            <div class="form-group col-md-6">
              <label for="releaseDate">Release Date</label>
              <input type="text" class="form-control" id="releaseDate" value="April 24, 2019">
            </div>
          </div>
          <div class="form-row">
            <div class="form-group col-md-4">
              <label for="domestic">Domestic</label>
              <input type="text" class="form-control" id="domestic" placeholder="" value="$858,373,000">
            </div>
            <div class="form-group col-md-4">
              <label for="international">International</label>
              <input type="text" class="form-control" id="international" placeholder="" value="$1,939,427,564">
            </div>
            <div class="form-group col-md-4">
              <label for="worldwide">Worldwide</label>
              <input type="text" class="form-control" id="worldwide" placeholder="" value="$2,797,800,564">
            </div>
          </div>
          <div class="form-row justify-content-center">
            <button type="submit" class="btn btn-primary mr-4">Create</button>
            <button type="submit" class="btn btn-primary mr-4">Update</button>
            <button type="submit" class="btn btn-primary">Delete</button>
          </div>
        </form>
      </div>
    </div>
  </div>
</div>
<div class="row justify-content-center pt-2">
  <div class="text-center col">
    <h1 class="h3">Feature : Template-driven Forms</h1>
    <hr>
  </div>
</div>
<div class="row justify-content-center p-2">
  <div class="col-12 col-sm-12 col-md-6 col-lg-6 col-xl-6">
    <div class="card">
      <div class="card-header text-center">
        <h4>Movie : <span class="text-primary font-weight-bold">Avengers: Endgame</span></h4>
      </div>
      <div class="card-body">
        <form>
          <div class="form-row">
            <div class="form-group col-md-6">
              <label for="name">Name</label>
              <input type="text" class="form-control" id="name" value="Avengers: Endgame">
            </div>
            <div class="form-group col-md-6">
              <label for="releaseDate">Release Date</label>
              <input type="text" class="form-control" id="releaseDate" value="April 24, 2019">
            </div>
          </div>
          <div class="form-row">
            <div class="form-group col-md-4">
              <label for="domestic">Domestic</label>
              <input type="text" class="form-control" id="domestic" placeholder="" value="$858,373,000">
            </div>
            <div class="form-group col-md-4">
              <label for="international">International</label>
              <input type="text" class="form-control" id="international" placeholder="" value="$1,939,427,564">
            </div>
            <div class="form-group col-md-4">
              <label for="worldwide">Worldwide</label>
              <input type="text" class="form-control" id="worldwide" placeholder="" value="$2,797,800,564">
            </div>
          </div>
          <div class="form-row justify-content-center">
            <button type="submit" class="btn btn-primary mr-4">Create</button>
            <button type="submit" class="btn btn-primary mr-4">Update</button>
            <button type="submit" class="btn btn-primary">Delete</button>
          </div>
        </form>
      </div>
    </div>
  </div>
</div>

Développer et expérimenter

Nous pouvons dès lors effectuer une série d'essais et de tests pour comprendre comment fonctionne les forms.

Commençons par créer une classe Simple (nommée Movie) et modifions la en fonction des données souhaitées.

# Création de la classe Movie dans le répertoire template-driven-forms
ng generate class modules/application/template-driven-forms/Movie
src/app/modules/application/template-driven-forms/movie.ts
export class Movie {
  name: string;
  releaseDate: string;
  domestic: string;
  international: string;
  worldwide: string;
}

La technique que nous allons utiliser est celle du binding.
Elle consiste à lier le formulaire au modèle.

Le modèle sera notre classe nous le déclarerons au niveau du fichier TypeScript.
Le formulaire sera contenu dans le fichier HTML.

Pour relier par exemple le champ name (input) nous utiliserons au choix deux types de syntaxe.

  • One way binding
    Les données vont du modèle vers le formulaire
    [ngModel]
     
  • Two way binding
    Les données vont du modèle vers le formulaire et du formulaire vers le modèle
    [(ngModel)]

Remarque
si la directive ngModel est utilisée il faut obligatoirement indiquer l'attribut name.


Rajoutons enfin la syntaxe {{ movie | json }}
Pour vérifier le flux de données entre le modèle et les input.
Ce qui donnera dans nos fichiers le code suivant.

src/app/modules/application/template-driven-forms/template-driven-forms.ts
import { Component, OnInit } from '@angular/core';
import { Movie } from './movie';

@Component({
  selector: 'app-template-driven-forms',
  templateUrl: './template-driven-forms.component.html',
  styleUrls: ['./template-driven-forms.component.css']
})
export class TemplateDrivenFormsComponent implements OnInit {
  movie: Movie = new Movie();

  constructor() {
    this.movie.name = 'Avengers : Endgame';
    this.movie.releaseDate = '04/04/2019';
    this.movie.domestic = '$858,373,000';
    this.movie.international = '$2,797,800,564';
    this.movie.worldwide = '$2,797,800,564';
  }

  ngOnInit() {
  }

}
src/app/modules/application/template-driven-forms/template-driven-forms.html
<div class="row justify-content-center pt-2">
  <div class="text-center col">
    <h1 class="h3">Feature : Template-driven Forms</h1>
    <hr>
  </div>
</div>
<div class="row justify-content-center p-2">
  <div class="col-12 col-sm-12 col-md-4 col-lg-4 col-xl-4">
    {{ movie | json }}
  </div>
  <div class="col-12 col-sm-12 col-md-8 col-lg-8 col-xl-8">
    <div class="card">
      <div class="card-header text-center">
        <h4>Movie : <span class="text-primary font-weight-bold">Avengers: Endgame</span></h4>
      </div>
      <div class="card-body">
        <form>
          <div class="form-row">
            <div class="form-group col-md-6">
              <label for="name">Name</label>
              <input type="text" class="form-control" name="name" id="name" [ngModel]="movie.name">
            </div>
            <div class="form-group col-md-6">
              <label for="release">Release Date</label>
              <input type="text" class="form-control" name="release" id="release"  [ngModel]="movie.releaseDate">
            </div>
          </div>
          <div class="form-row">
            <div class="form-group col-md-4">
              <label for="domestic">Domestic</label>
              <input type="text" class="form-control" name="domestic" id="domestic" [(ngModel)]="movie.domestic">
            </div>
            <div class="form-group col-md-4">
              <label for="international">International</label>
              <input type="text" class="form-control" name="international" id="international"  [(ngModel)]="movie.international">
            </div>
            <div class="form-group col-md-4">
              <label for="worldwide">Worldwide</label>
              <input type="text" class="form-control" name="worldwide" id="worldwide" [(ngModel)]="movie.worldwide">
            </div>
          </div>
          <div class="form-row justify-content-center">
            <button type="submit" class="btn btn-primary mr-4">Create</button>
            <button type="submit" class="btn btn-primary mr-4">Update</button>
            <button type="submit" class="btn btn-primary">Delete</button>
          </div>
        </form>
      </div>
    </div>
  </div>
</div>
src/app/modules/application/template-driven-forms/template-driven-forms.spec.ts
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { TemplateDrivenFormsComponent } from './template-driven-forms.component';
import { FormsModule } from '@angular/forms';

describe('TemplateDrivenFormsComponent', () => {
  let component: TemplateDrivenFormsComponent;
  let fixture: ComponentFixture<TemplateDrivenFormsComponent>;

  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [
        FormsModule,
      ],
      declarations: [ TemplateDrivenFormsComponent ]
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(TemplateDrivenFormsComponent);
    component = fixture.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });
});

ngForm

En HTML un sélecteur ID (ID selector) est un nom précédé du caractère hash (#).

ngForm est une directive angular qui va nous permettre de visualiser les données du formulaire.
L'accès à chaque input va se faire via le DOM directement et les sélecteurs d'id.
Par exemple sur le input dédié au champ name on rajoute dans le DOM
#name

Nous pourrons dès lors accèder aux input directement dans le code HTML.


Tests

Il ne reste plus qu'à tester les différents scripts Angular.

# Développement
npm run start
http://localhost:4200/

# Tests
npm run lint
npm run test
npm run e2e

# Compilation
npm run build