#CestFacile : Validation de formulaire (et bien plus) avec Formsy React

Rappel des faits :

  • On a vu comment utiliser Redux et Redux Sagas dans une application créée avec Create-React-App (CRA, pour les intimes)
  • On a vu comment écrire des tests unitaires
  • On sait utiliser React Router, bricoler des Protected routes et utiliser un layout commun
  • On a même vu rapidement le pattern Render Props qui est à mon avis voué à se répandre de plus en plus dans les mois à venir

Bref, on a vu pas mal de choses, mais on a jusqu’ici :

  • Une app qui affiche un formulaire dont les champs ne sont même pas obligatoires
  • Une page principale qui affiche du contenu statique

On vaut mieux que ça.

Forsmy

On a ce formulaire, qui n’est régit par aucune règle. On pourrait se content d’un « required » bien senti au niveau de chaque champ, mais pourquoi ne pas utiliser quelque chose de plus cool ?

On va utiliser Formsy.

Formsy permet de :

  • Créer essentiellement des inputs en bénéficiant d’une validation intégrée
  • Ajouter dynamiquement des champs dans un formulaire
  • Créer de nouvelles règles de validation de champ, par exemple des règles complexes comme la validation de numéros de téléphone
  • Faire un check de champ de formulaire à partir d’une API, par exemple pour vérifier la validité d’un username

Son fonctionnement dans sa forme basique est très simple :

  • On créé un Component Input
  • On le wrap dans HOC Formsy
  • On appelle ce composant dans un formulaire <Formsy />, ce qui permet de bénéficier de différents handlers (error, submit, valid..)

Notre Input « générique » :

 

import React from 'react';
import { withFormsy } from 'formsy-react';
import {Form} from 'semantic-ui-react'

import './MyInput.css'

class MyInput extends React.Component {
  changeValue = (event) => {
    this.props.setValue(event.currentTarget.value);
  }

  render() {
    const errorMessage = this.props.getErrorMessage();
    let inErrorClassName = (!this.props.isValid() && !this.props.isPristine()) ? 'inputError' : ''

    return (
      <div>
        <Form.Input 
          fluid 
          name={this.props.name}
          label={this.props.label} 
          onChange={this.changeValue} 
          placeholder={this.props.placeholder} 
          value={this.props.getValue() || ''}
          className={inErrorClassName}
        />
        <span className="inputError">{errorMessage}</span>
      </div>
    );
  }
}

export default withFormsy(MyInput);

Dans ./src/Components/MyInput/MyInput.js

Notre formulaire de login ressemblera alors à :

See the Pen MQLQYX by daibai (@Daibai) on CodePen.

Custom Validation

Admettons que vous souhaitiez ajouter une règle de validation spécifique à votre projet, par exemple une validation basée sur l’âge (minimum 18 ans).

Formsy permet de mettre en place des règles custom très facilement, il suffit d’ajouter une nouvelle règle de validation via addValidationRule, puis de la déclarer dans votre input :

import Formsy, {addValidationRule} from 'formsy-react';

addValidationRule('isAdult', (values, value) => {
 const currentYear = new Date().getFullYear();
 const parsedValue = parseInt(value, 10);
 if (typeof parsedValue !== 'number') {
  return false;
 }
 return parsedValue < (currentYear - 18);
});

<MyInput
  name="number"
  validations="isAdult"
  label="Année de naissance"
  placeholder="Année de naissance"
  validationError="Vous êtes trop jeune"
  required
 />

Vous pouvez bien sûr passer plusieurs règles de validation pour un même champ :

<MyInput name="number" validations={{
  isNumeric: true,
  isLength: 5
}}/>

Champs dynamiques

Imaginons que vous ayez à gérer l’ajout dynamique de champs au sein de votre formulaire. Ce n’est pas si exotique que ça, en fait ça arrive fréquemment, par exemple quand vous devez ajouter chaque membre de votre famille pour un abonnement, un voyage, un formulaire administratif…

Ca tient en quelques lignes , vous avez un formulaire Formsy qui contient les champs dynamiques à ajouter. Au submit, on ajoute ces champs dans le formulaire « principal » :

// Form d'ajout de champ
 <Formsy onSubmit={this.addField}>
   <MyInput
     name="email"
     validations="isEmail"
     label="Adresse E-Mail"
     placeholder="E-Mail"
     validationError="Adresse E-Mail non valide"
     required
   />
   <button type="submit">Ajouter un champ E-Mail</button>
 </Formsy>

// Formulaire qui contient les champs ajoutés 
 <Formsy onSubmit={this.submit} onValid={this.enableButton} onInvalid={this.disableButton}>
   <Fields data={fields} onRemove={this.removeField} />
   <button type="submit" disabled={!canSubmit}>Submit</button>
 </Formsy>

La fonction addField:

 addField(fieldData) {
   fieldData.validations = fieldData.validations.length ?
     fieldData.validations.reduce((a, b) => Object.assign({}, a, b)) 
     : null;
   fieldData.id = Date.now();
   this.setState({ fields: this.state.fields.concat(fieldData) });
 }

Notre state :

this.state = { fields: [], canSubmit: false };

Le composant Fields gère l’affichage des champs contenus dans le state :

<div>
 {props.data.map((field, i) => (
   <div className="field" key={field.id}>
   {
     <MyInput
       value=""
       name={`fields[${i}]`}
       title={field.validations ? JSON.stringify(field.validations) : 'No validations'}
       required={field.required}
       validations={field.validations}
     />
   }
   </div>
  ))
 }
</div>

Fini ?

Pas vraiment. Si vous suivez mes posts depuis le début, vous avez probablement des tests qui trainent dans ./src/Scenes/Login/Login.test.js

Si vous tentez de commit, votre hook GIT va vous envoyer bouler avec une erreur comme celle-ci :

  <Login /> container › should dispatch a login action when form in submited

    AssertionError: expected false to be true

Pas de panique, c’est normal.

Maintenant que l’on a une validation de form sur le login, on ne peut plus se contenter d’envoyer un login et un password vide, on doit remplir nos champs pour tester la soumission du form :

it('should dispatch a login action when form in submited', () => {
 const action = {
  type: 'LOGIN',
  login :'[email protected]', 
  password: '[email protected]'
 }
 const wrapper = mount(<Login store={store} />)

 const login = wrapper.find('MyInput[name="email"]');
 const password = wrapper.find('MyInput[name="password"]');

 login.instance().props.setValue(action.login)
 password.instance().props.setValue(action.password)
 
 const button = wrapper.find('FormButton')
 button.simulate('submit')
 expect(store.isActionDispatched(action)).to.be.true;
})

Dans ./src/Scenes/Login/Login.test.js

On récupère nos inputs dans le composant, puis on set leur valeur avec celles attendues dans notre test.

Ca nous amène à un autre test : on veut s’assurer que l’on ne peut pas soumettre le formulaire si les champs sont vides, ou si le champ email n’est pas dans le format attendu :

 

See the Pen OQdEbm by daibai (@Daibai) on CodePen.

On est à nouveau green, on peut à nouveau commit 🙂

Le code est ici :

https://github.com/GregoryBabonaux/react-c-est-facile/tree/2e833b2d13f833e0eb1704f56fa5a0fe48b2e759

 

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *