Valider les champs d’un formulaire est toujours un peu galère. Mais il existe existe un framework simple, compréhensible et extensible: Yup. Découverte.

Yup, le framework facile

Yup est un valideur de schéma, c’est à dire qu’il va contrôler que les valeurs passées en paramètres respectent les règles configurées. On s’évite ainsi de définir une fonction globale pour tester les champs d’un formulaire.

Exemples de contrôles

  • obligatoire, non null
  • string, number, date, email
  • positif, négatif, entier

Il est bien sûr possible d’ajouter ses propres tests pour aller plus loin (exemple: égalité de deux mots de passe).

Intégration avec Svelte

Il faut d’abord installer le paquet avec NPM via la commande npm install yup. Ensuite, dans chaque composant possédant un formulaire, il faut inclure la librairie yup import * as yup from 'yup'; puis définir le schéma de validation

    
1let userSchema = yup.object().shape( {
2  name: string().required(),
3  age: number().required().positive().integer(),
4  email: string().email(),
5  website: string().url().nullable(),
6  createdOn: date().default(() => new Date()),
7} );

Finalement, on lance la validation par

    
1await userSchema.validate( { name: 'test', age: 15, email: 'test@test.com', website: null, createdOn: date() } );

Application à la page de login de LoFiDroX

Sans Yup

Le code initial du composant de login du projet LoFiDroX est le suivant:

    
  1<style>
  2    @import "../../frameworks/bootstrap/css/bootstrap.min.css";
  3    @import "../css/lofidrox.css";
  4</style>
  5
  6<script>
  7    import { navigate } from "svelte-routing";
  8    import { onMount } from 'svelte';
  9    import GtgUtilsTools from '../../frameworks/gtg-svelte/gtg-utils-tooling.js';
 10    import GtgUtilsPassword from '../../frameworks/gtg-svelte/gtg-utils-password.js';
 11    import GtgUtilsWeb from '../../frameworks/gtg-svelte/gtg-utils-web.js';
 12    import LfdUrls from '../js/lfdUrlManager.js';
 13    import LfdEvents from '../js/lfdEventsManager.js';
 14    import LfdLocalStore from '../js/lfdLocalStore.js';
 15    import { lfdStore } from '../js/lfdStore.js';
 16
 17    let fields = { fieldUsr: null, fieldPwd: null };
 18    let values = { username: null, password: null };
 19    let validation = { user: true, pwd: true, login: true };
 20
 21    function checkUsername() {
 22        return (!GtgUtilsTools.isNull(values.username) && (4 <= values.username.length));
 23    };
 24
 25    function checkPassword() {
 26        return GtgUtilsPassword.checkStrongPwd(values.password);
 27    };
 28
 29    function login() {
 30        validation.login = true;
 31        validation.user = checkUsername();
 32        validation.pwd = checkPassword();
 33        if (validation.user && validation.pwd) {
 34            GtgUtilsTools.setLocalValue(LfdLocalStore.field_username, values.username);
 35            GtgUtilsWeb.postJson(LfdUrls.crud_user_login, { 'username': values.username, 'pwd': values.password }, function(data) {
 36                if (!GtgUtilsTools.isNull(data) && !GtgUtilsTools.isNull(data.logged) && data.logged) {
 37                    $lfdStore.loggedIn = true;
 38                    GtgUtilsWeb.route(LfdUrls.spa_home);
 39                }
 40            }, function(error) {
 41                if (!GtgUtilsTools.isNull(error) && (GtgUtilsWeb.HTTP_CODES.Unauthorized === error)) {
 42                    validation.login = false;
 43                } else {
 44                    GtgUtilsWeb.route(LfdUrls.spa_error);
 45                }
 46            } );
 47        }
 48    };
 49
 50    function goRegister() {
 51        GtgUtilsWeb.route(LfdUrls.spa_user_register);
 52    };
 53
 54    values.username = window.localStorage.getItem(LfdLocalStore.field_username);
 55
 56    onMount(() => {
 57        if (!GtgUtilsTools.isNull(values.username)) {
 58            fields.fieldPwd.focus();
 59        } else {
 60            fields.fieldUsr.focus();
 61        }
 62    } );
 63</script>
 64
 65<main class="container">
 66    <section class="h-100 lfd-gradient-form">
 67        <div class="container py-2 h-100">
 68            <div class="row d-flex justify-content-center align-items-center h-100">
 69                <div class="col-xl-10">
 70                    <div class="card rounded-3 text-black">
 71                        <div class="row g-0">
 72                            <div class="col-md-6">
 73                                <div class="card-body p-md-5 mx-md-4">
 74                                    <div class="text-center">
 75                                        <img src="/lofidrox/img/lofidrox.png" alt="LoFiDroX" class="lfd-logo">
 76                                        <h4 class="mt-1 mb-5 pb-1"></h4>
 77                                    </div>
 78                                    <form on:submit|preventDefault|stopPropagation={(e)=>{login();}}>
 79                                        <p>Please login to your account</p>
 80                                        <div class="form-outline mb-4">
 81                                            <input class="input form-control form-control-sm" tabindex="1" type="text" name="fieldUsr" placeholder="Enter your username" bind:value={values.username} bind:this={fields.fieldUsr}>
 82                                            {#if !validation.user }
 83                                            <p class="bg-danger text-white px-1 py-1">Username must contains at least 4 letters</p>
 84                                            {/if}
 85                                        </div>
 86                                        <div class="form-outline mb-4">
 87                                            <input class="input form-control form-control-sm" tabindex="2" type="password" name="fieldPwd" placeholder="Enter your password" bind:value={values.password} bind:this={fields.fieldPwd}>
 88                                            {#if !validation.pwd }
 89                                            <p class="bg-danger text-white px-1 py-1">
 90                                                Password must contains uppercase letter, lowercase letter, digit, special caracter and must be 10 to 256 caracters long.
 91                                            </p>
 92                                            {/if}
 93                                        </div>
 94                                        {#if !validation.login }
 95                                        <div class="form-outline mb-4">
 96                                            <p class="bg-danger text-white px-1 py-1">
 97                                                You cannot login. Please check your login and password.
 98                                            </p>
 99                                        </div>
100                                        {/if}
101                                        <div class="text-center pt-1 mb-5 pb-1">
102                                            <button class="btn btn-primary btn-block fa-lg lfd-gradient-colors mb-3" tabindex="3" type="submit">Log in&nbsp;<i class="mdi mdi-login" aria-hidden="true"></i></button>
103                                        </div>
104                                        <div class="d-flex align-items-center justify-content-center pb-4">
105                                            <p class="mb-0 me-2">Don't have an account?</p>
106                                            <a type="button" class="btn btn-outline-primary" on:click="{goRegister}" tabindex="4">Create&nbsp;<i class="mdi mdi-account-plus"></i></a>
107                                        </div>
108                                    </form>
109                                </div>
110                            </div>
111                            <div class="col-md-6 d-flex align-items-center lfd-gradient-colors">
112                                {#if validation.login }
113                                    <div class="text-white px-3 py-4 p-md-5 mx-md-4">
114                                        <p class="small mb-0"><span class="lfd-app-name">LoFiDroX</span>&nbsp;let you share files over a Local Area Network.&nbsp;
115                                            <span class="lfd-app-name">LoFiDroX</span>&nbsp;don't use cloud to synchronize content: files are stored on you local&nbsp;
116                                            <span class="lfd-app-name">LoFiDroX</span>&nbsp;instance.
117                                        </p>
118                                    </div>
119                                {:else}
120                                    <img src="/lofidrox/img/user_login_error.png" class="lfd-image-illu" alt="Error">
121                                {/if}
122                            </div>
123                        </div>
124                    </div>
125                </div>
126            </div>
127        </div>
128    </section>
129</main>

Dans la fonction login(), on voit la validation des deux champs par validation.user = checkUsername(); et validation.pwd = checkPassword();. En soit, cela fonctionne bien, surtout parce qu’il n’y a que deux champs à valider. Si on doit appliquer la même technique à un formulaire plus conséquent, le code va devenir une litanie de _validation.X = checkFieldX();_ et le risque d’erreur va augmenter rapidement (un champ oublié, un contrôle dupliqué).

Avec Yup

En utilisant Yup, le code devient:

    
  1<style>
  2    @import "../../frameworks/bootstrap/css/bootstrap.min.css";
  3    @import "../css/lofidrox.css";
  4</style>
  5
  6<script>
  7    import { navigate } from "svelte-routing";
  8    import { onMount } from 'svelte';
  9    import * as yup from 'yup';
 10    import GtgUtilsTools from '../../frameworks/gtg-svelte/gtg-utils-tooling.js';
 11    import GtgUtilsPassword from '../../frameworks/gtg-svelte/gtg-utils-password.js';
 12    import GtgUtilsWeb from '../../frameworks/gtg-svelte/gtg-utils-web.js';
 13    import LfdUrls from '../js/lfdUrlManager.js';
 14    import LfdEvents from '../js/lfdEventsManager.js';
 15    import LfdLocalStore from '../js/lfdLocalStore.js';
 16    import { lfdStore } from '../js/lfdStore.js';
 17
 18    let fields = { fieldUsr: null, fieldPwd: null };
 19    let values = { username: null, password: null };
 20    let validation = { login: true };
 21    let errorMessages = { username: null, pwd: null };
 22
 23    let schemaValidation = yup.object().shape( {
 24        username: yup.string().nullable().required('You must enter a username').min(4, 'Username must contains at least ${min} letters'),
 25        pwd: yup.string().nullable().required('You must enter a password').
 26            min(10, 'Password must contains at least ${min} caracters').
 27            max(256, 'Passqord must contains up to ${max} caracters long').
 28            test('pwd-strength', 'Password must contains uppercase letter, lowercase letter, digit, special caracter',
 29            function(value) {
 30                return GtgUtilsPassword.checkStrongPwd(value);
 31            } )
 32    } );
 33
 34    async function login() {
 35        try {
 36            validation.login = true;
 37            errorMessages = { username: null, pwd: null };
 38            await schemaValidation.validate( { 'username': values.username, 'pwd': values.password }, { abortEarly: false } );
 39            GtgUtilsTools.setLocalValue(LfdLocalStore.field_username, values.username);
 40            GtgUtilsWeb.postJson(LfdUrls.crud_user_login, { 'username': values.username, 'pwd': values.password }, function(data) {
 41                if (!GtgUtilsTools.isNull(data) && !GtgUtilsTools.isNull(data.logged) && data.logged) {
 42                    $lfdStore.loggedIn = true;
 43                    GtgUtilsWeb.route(LfdUrls.spa_home);
 44                }
 45            }, function(error) {
 46                if (!GtgUtilsTools.isNull(error) && (GtgUtilsWeb.HTTP_CODES.Unauthorized === error)) {
 47                    validation.login = false;
 48                } else {
 49                    GtgUtilsWeb.route(LfdUrls.spa_error);
 50                }
 51            } );
 52        } catch (errors) {
 53            errorMessages = GtgUtilsTools.turnYupErrorsToArray(errors);
 54        }
 55    };
 56
 57    function goRegister() {
 58        GtgUtilsWeb.route(LfdUrls.spa_user_register);
 59    };
 60
 61    values.username = window.localStorage.getItem(LfdLocalStore.field_username);
 62
 63    onMount(() => {
 64        if (!GtgUtilsTools.isNull(values.username)) {
 65            fields.fieldPwd.focus();
 66        } else {
 67            fields.fieldUsr.focus();
 68        }
 69    } );
 70</script>
 71
 72<main class="container">
 73    <section class="h-100 lfd-gradient-form">
 74        <div class="container py-2 h-100">
 75            <div class="row d-flex justify-content-center align-items-center h-100">
 76                <div class="col-xl-10">
 77                    <div class="card rounded-3 text-black">
 78                        <div class="row g-0">
 79                            <div class="col-md-6">
 80                                <div class="card-body p-md-5 mx-md-4">
 81                                    <div class="text-center">
 82                                        <img src="/lofidrox/img/lofidrox.png" alt="LoFiDroX" class="lfd-logo">
 83                                        <h4 class="mt-1 mb-5 pb-1"></h4>
 84                                    </div>
 85                                    <form on:submit|preventDefault|stopPropagation={(e)=>{login();}}>
 86                                        <p>Please login to your account</p>
 87                                        <div class="form-outline mb-4">
 88                                            <input class="input form-control form-control-sm" tabindex="1" type="text" name="fieldUsr" placeholder="Enter your username" bind:value={values.username} bind:this={fields.fieldUsr}>
 89                                            {#if errorMessages.username }
 90                                            <p class="bg-danger text-white px-1 py-1">{errorMessages.username}</p>
 91                                            {/if}
 92                                        </div>
 93                                        <div class="form-outline mb-4">
 94                                            <input class="input form-control form-control-sm" tabindex="2" type="password" name="fieldPwd" placeholder="Enter your password" bind:value={values.password} bind:this={fields.fieldPwd}>
 95                                            {#if errorMessages.pwd }
 96                                            <p class="bg-danger text-white px-1 py-1">{errorMessages.pwd}</p>
 97                                            {/if}
 98                                        </div>
 99                                        {#if !validation.login }
100                                        <div class="form-outline mb-4">
101                                            <p class="bg-danger text-white px-1 py-1">
102                                                You cannot login. Please check your login and password.
103                                            </p>
104                                        </div>
105                                        {/if}
106                                        <div class="text-center pt-1 mb-5 pb-1">
107                                            <button class="btn btn-primary btn-block fa-lg lfd-gradient-colors mb-3" tabindex="3" type="submit">Log in&nbsp;<i class="mdi mdi-login" aria-hidden="true"></i></button>
108                                        </div>
109                                        <div class="d-flex align-items-center justify-content-center pb-4">
110                                            <p class="mb-0 me-2">Don't have an account?</p>
111                                            <a type="button" class="btn btn-outline-primary" on:click="{goRegister}" tabindex="4">Create&nbsp;<i class="mdi mdi-account-plus"></i></a>
112                                        </div>
113                                    </form>
114                                </div>
115                            </div>
116                            <div class="col-md-6 d-flex align-items-center lfd-gradient-colors">
117                                {#if validation.login }
118                                    <div class="text-white px-3 py-4 p-md-5 mx-md-4">
119                                        <p class="small mb-0"><span class="lfd-app-name">LoFiDroX</span>&nbsp;let you share files over a Local Area Network.&nbsp;
120                                            <span class="lfd-app-name">LoFiDroX</span>&nbsp;don't use cloud to synchronize content: files are stored on you local&nbsp;
121                                            <span class="lfd-app-name">LoFiDroX</span>&nbsp;instance.
122                                        </p>
123                                    </div>
124                                {:else}
125                                    <img src="/lofidrox/img/user_login_error.png" class="lfd-image-illu" alt="Error">
126                                {/if}
127                            </div>
128                        </div>
129                    </div>
130                </div>
131            </div>
132        </div>
133    </section>
134</main>

Dans la méthode login(), les appels _validation.X = checkFieldX()_ ont disparus au profit de la méthode await schemaValidation.validate(). Cette méthode va contrôler l’ensemble des valeurs des champs par rapport aux règles déclarées. La plupart des contrôles sont intégrés (longueur, obligatoire, etc) et il est facile d’ajouter une fonction spécifique: ici j’ai ajouté un contrôle de la qualité du mot de passe.

Conclusion

Yup est un framework de validation qui s’intégre facilement dans les applications et propose la plupart des contrôles basiques nécessaires. De plus il est également facile d’ajouter des contrôles personnalisés. Bref, c’est un indispensable.