Validation de formulaire avec Aurelia

Aurelia n'est pas encore en beta mais elle propose déjà de très puissants plugins. J'ai découvert aujourd'hui aurelia-validation qui permet, comme son nom l'indique, de valider des données issues de formulaires.

Le principe est donc de spécifier toutes les étapes de validations de chaque champs et de tester la validité de l'ensemble du groupe de contrôles lors de la soumission du formulaire avant de faire la requête finale auprès du backend.

Chez Angular, tout cela se fait à l'aide de ng-model et autres attributs minlength. Cela avait l'inconvénient de mettre de la logique métier dans le template. Avec Aurelia, la séparation des contextes est toujours maintenue, et c'est dans le contrôleur que nous allons spécifier tout ça. Le template, lui, se contente de rendre le formulaire sans se soucie de quoi que ce soit relatif à la validation.

Je vous montre uniquement l'exemple avec les décorateurs car c'est celui qui me plait le plus, mais je vous invite à aller regarder toutes les possibilités que vous offre ce module et surtout, d'aller regarder les exemples live.

Une fois aurelia-validation installé, pour l'utiliser, il suffira de l'injecter à la classe qui contrôle la vue de votre formulaire et de lui spécifier des propriétés décorées :

import {Validation} from 'aurelia-validation';  
import {ensure} from 'aurelia-validation';  
import {inject} from 'aurelia-framework';

@inject(Validation)
export class RegisterViewModel {  
  @ensure(function(it){ it.isNotEmpty().hasLengthBetween(3,10) })
  name = '';
  @ensure(function(it){ it.isNotEmpty().isEmail() })
  email = '';

  constructor(validation) {
    this.validation = validation.on(this);
  }

  save(){
    this.validation.validate()
    .then(() => this.user.save());
  }
}
<template>  
<form  
  role="form"
  submit.delegate="save()"
  validate.bind="validation">
  <div
    class="form-group">
    <label>Your name</label>
    <input
      type="text"
      value.bind="name"
      class="form-control" >
  </div>
  <div
    class="form-group">
    <label>Your email address</label>
    <input
      type="text"
      value.bind="email"
      class="form-control" >
  </div>
  <button
    type="submit"
    class="btn btn-default">Submit</button>
</form>  
</template>  

Par défaut, aurelia-validator va ajouter un markup spécifique pour afficher les erreurs. On peut personnaliser ce markup en ajoutant du code dans le template comme :

<form  
  class="row"
  submit.delegate="save()"
  validate.bind="validation"
  novalidate>

  <div
    class="input-field row">
    <div
      class="col s12">
      <input
        id="email"
        type="email"
        value.bind="email"
        class="validate ${validation.result.properties.email.isDirty && !validation.result.properties.email.isValid ? 'invalid' : ''}">
      <label
        for="email"
        data-error.bind="validation.result.properties.email.isDirty && validation.result.properties.email.message"
        data-success="√"
        t="email">
      </label>
    </div>
  </div>
</form>  

qui permet donc d'afficher les erreurs avec le framework materialize. Mais c'est vilain de mettre ce genre de code dans le template, et en plus, j'ai dit au début que c'était prévu pour séparer la logique de la vue.

On va donc configurer un ValidationViewStrategy dans notre formulaire. Par défaut, aurelia-validation propose TWBootstrapViewStrategy qui adapte donc les messages d'erreurs à Bootstrap. Dans mon projet, j'utilise materialize. Il me fallait donc utiliser une autre stratégie. Voici donc comment on fait.

La première chose à faire est de définir une classe héritant de ValidationViewStrategy. Il ne restera qu'à définir ses méthodes prepareElement et updateElement qui seront appelées lors de la création et lors de la mise à jour de chacun des contrôles.

Voici le résultat pour que ça fonctionne avec materialize :

import {ValidationViewStrategy} from 'aurelia-validation';

export class MaterializeViewStrategyBase extends ValidationViewStrategy {

  updateUi (validationProperty, element) {
    const label = element.parentNode.querySelector('label');
    if (label) {
      label.dataset.success = '√';
    }

    if (validationProperty) {
      if (!validationProperty.isDirty) {
        return;
      }
      if (validationProperty.isValid) {
        element.classList.remove('invalid');
        element.classList.add('valid');
      } else {
        element.classList.remove('valid');
        element.classList.add('invalid');

        if (label) {
          label.dataset.error = validationProperty.message;
        }
      }
    }
  }

  prepareElement(validationProperty, element) {
    this.updateUi(null, element);
  }

  updateElement(validationProperty, element) {
    this.updateUi(validationProperty, element);
  }
}

export const MaterializeViewStrategy = new MaterializeViewStrategyBase();  

Il ne reste plus qu'à configurer notre validateur dans notre contrôleur :

import {inject} from 'aurelia-framework';  
import {Validation} from 'aurelia-validation';  
import {MaterializeViewStrategy} from '../ui/forms/materialize-view-strategy';

@inject(Validation)
export class RegisterViewModel {  
  constructor(validation) {
    validation.config.useViewStrategy(MaterializeViewStrategy);
    this.validation = validation.on(this);
  }
}

Et voici le résultat :

Hadrien

Hi, I'm a french Javascript Lead Developer, Web Architect from Toulouse, France. I've worked for 12 years for many projects with YUI, AngularJS, Aurelia.io and now React and React native.

Toulouse, France https://hadrien.eu