22.2.
De meilleurs formulaires avec VeeValidate
Écrire notre application en JavaScript nous permet de faire de la validation côté client. Ce n’est pas
un remplacement pour la validation côté serveur, mais c’est un plus pour les utilisateurs de savoir
instantanément que le formulaire est invalide, sans avoir à attendre de soumettre le formulaire et
obtenir une réponse du serveur.
VeeValidate est dédiée à cette validation côté client. Elle fonctionne avec la directive v-model que
nous venons de découvrir, et y ajoute quelques fonctionnalités de validation bien pratiques.
VeeValidate vient avec de nombreuses règles de validation, mais aucune d’elles n’est incluse dans
l’application par défaut. Elles sont fournies dans un package séparé appelé @vee-validate/rules.
Cela permet de garder l’application aussi légère que possible, mais nous oblige à déclarer
explicitement quelles règles de validation doivent être incluses. Tu peux les importer toutes d’un
coup, mais ce n’est pas une très bonne idée, parce que tu n’en utiliseras sans doute que quelques-
unes.
Voici les règles de validation disponibles qui sont le plus souvent utilisées :
• required pour rendre un champ obligatoire ;
• min:N et max:N pour s’assurer qu’une valeur saisie a au moins/au plus N caractères ;
• min_value:N, max_value:N, between pour s’assurer qu’un nombre est au moins/au plus égal à N, ou
est compris entre deux bornes ;
• email pour vérifier que l’utilisateur a saisi une adresse email valide ;
• url pour vérifier que l’utilisateur a saisi URL valide ;
• alpha, alpha_num, alpha_dash, alpha_space pour s’assurer qu’une valeur est alphabétique
seulement/avec des nombres/avec des tirets/avec des espaces ;
• numeric pour vérifier que la valeur saisie est un nombre ;
• regex pour s’assurer qu’une valeur correspond à une expression régulière donnée.
Pour les input type="file", il en existe d’autres spécifiques :
• mimes permet de définir quels types MIME sont acceptés pour le fichier ;
• ext permet de lister les extensions acceptées ;
• size permet de fixer une taille de fichier maximale ;
• image n’autorise que des formats d’image ;
• dimensions permet de définir les dimensions précises requises pour une image.
Certaines règles permettent de faire de la validation sur plusieurs champs :
• required_if rend un champ obligatoire si un autre est rempli avec une des valeurs données ;
• confirmed permet de s’assurer qu’un champ a la même valeur qu’un autre (double vérification
classique pour un mot de passe).
Pour pouvoir utiliser une quelconque de ces règles de validation dans ton application, tu dois la
181
charger explicitement :
[Link]
import { defineRule } from 'vee-validate';
import { confirmed, min, required } from '@vee-validate/rules';
defineRule('min', min);
defineRule('required', required);
defineRule('confirmed', confirmed);
Comme tu peux le deviner grâce à cet exemple, nous pourrions renommer les règles de validation
comme bon nous semble. Mais, en règle générale, j’utilise les noms prédéfinis. Maintenant que nous
avons chargé les règles désirées, nous pouvons les utiliser dans nos composants pour valider des
champs de formulaire.
On peut aussi configurer VeeValidate, par exemple pour indiquer quand valider les champs. Par
défaut, la librairie vérifie les champs sur les évènements blur et change. Mais j’aime bien valider sur
l’évènement input également :
[Link]
import { configure } from 'vee-validate';
configure({
validateOnInput: true,
});
VeeValidate propose deux modes d’utilisation :
• un mode basé sur l’API de Composition,
• un mode basé sur des composants à utiliser dans les templates (un mode de fonctionnement
que l’on appelle des Higher Order Components, ou HOC, pour composants de plus haut ordre).
22.2.1. VeeValidate avec l’API de Composition
VeeValidate offre une fonction pour déclarer un champ de formulaire : useField().
[Link]
import { useField } from 'vee-validate';
const { value: name, errorMessage: nameErrorMessage, meta: nameMeta } = useField('
name', { required: true });
L’objet retourné par useField() a plusieurs propriétés intéressantes, comme value, la valeur du
champ, que l’on peut utiliser dans un v-model. VeeValidate gère également la validation du champ,
et offre une propriété errorMessage, qui contient le potentiel message d’erreur du champ.
182
[Link]
<template>
<label for="name">Name</label>
<input id="name" v-model="name" name="name" />
<div v-if="[Link] && ![Link]" class="error">{{ nameErrorMessage
}}</div>
</template>
<script setup lang="ts">
import { useField } from 'vee-validate';
const { value: name, errorMessage: nameErrorMessage, meta: nameMeta } = useField('
name', { required: true });
</script>
useField offre en fait plus de propriétés que ça grâce à meta :
• valid est un booléen qui indique si le champ est valide ou invalide ;
• dirty est un booléen qui indique si le champ est "vierge" (l’utilisateur n’a rien saisi) ou "sale"
(l’utilisateur a saisi une valeur différente de la valeur initiale) ;
• touched est un booléen qui indique si le champ a perdu le focus ou pas ;
VeeValidate aide également à valider l’état du formulaire dans son ensemble, et fournit pour cela
une autre fonction useForm().
[Link]
import { useField, useForm } from 'vee-validate';
const { meta: formMeta, handleSubmit, errors } = useForm();
// add a `name` field to the form, initialized with 'JB'
const { value: name } = useField('name', { required: true }, { initialValue: 'JB' });
// add a `password` field to the form
const { value: password } = useField('password', { required: true });
// the register function has access to the form values directly
const register = handleSubmit(values => {
// this will only be called if the form is valid
[Link](values);
});
Tu peux enregistrer autant de champs que tu le désires. useForm renvoie un objet avec une fonction
handleSubmit qui nous sert à définir comment gérer la soumission, un object errors contenant les
erreurs de validation, et les mêmes propriétés que useField dans une propriété meta. La logique est
assez simple à comprendre : valid est true si tous les champs sont valides, et false si au moins un
champ est invalide ; dirty est false si tous les champs sont "vierges", et true si au moins un champ
est "sale" , etc.
On peut par exemple désactiver le bouton de soumission du formulaire s’il est invalide :
183
[Link]
<template>
<form @submit="register()">
<label for="name">Name</label>
<input id="name" v-model="name" />
<div class="error">{{ [Link] }}</div>
<label for="password">Password</label>
<input id="password" v-model="password" type="password" />
<div class="error">{{ [Link] }}</div>
<!-- button is disabled while the form is invalid -->
<button :disabled="![Link]">Register</button>
</form>
</template>
<script setup lang="ts">
/* eslint-disable no-console */
import { useField, useForm } from 'vee-validate';
const { meta: formMeta, handleSubmit, errors } = useForm();
// add a `name` field to the form, initialized with 'JB'
const { value: name } = useField('name', { required: true }, { initialValue: 'JB' });
// add a `password` field to the form
const { value: password } = useField('password', { required: true });
// the register function has access to the form values directly
const register = handleSubmit(values => {
// this will only be called if the form is valid
[Link](values);
});
</script>
L’API de composition proposée par VeeValidate fait le gros du travail en validant les champs et en
calculant l’état du formulaire. C’est puissant et simple à utiliser. Et, comme nous allons le voir dans
la partie suivante, il y a une autre façon, encore plus simple, d’utiliser VeeValidate.
22.2.2. VeeValidate avec des composants de plus haut ordre
Au lieu d’utiliser l’API de Composition, VeeValidate offre un composant pour définir un champ dans
le template, et ajouter de la validation sur celui-ci. Ce composant s’appelle… Field ! Field ne fait pas
grand-chose en lui-même. Mais on peut l’utiliser avec un autre composant offert par VeeValidate :
Form. Form émet un événement submit avec les valeurs du formulaire :
[Link]
<Form @submit="register($event)">
<label for="name">Name</label>
<Field id="name" name="name" rules="required" value="JB" />
<label for="password">Password</label>
184
<Field id="password" name="password" type="password" rules="required" />
<button type="submit">Register</button>
</Form>
Tu as juste à coder la fonction qui gère la soumission :
[Link]
import { Field, Form } from 'vee-validate';
function register(values: Record<string, unknown>) {
// { name: string; password: string }
[Link](values);
}
Comme tu peux le voir, Field attend simplement un nom, et (par défaut) affiche un input. Si tu veux
donner une valeur initiale au champ, tu peux utiliser la prop value. Ou tu peux aussi utiliser v-
model : cela peut être très pratique si tu veux réagir à un changement de valeur ! Tu as alors
simplement besoin d’une valeur réactive et d’un watcher.
[Link]
const user = reactive({ name: 'Cédric', password: '' });
const passwordStrength = computed(() => computeStrength([Link]));
Et d’ajouter le v-model sur ton Field :
[Link]
<Field id="password" v-model="[Link]" name="password" type="password"
rules="required" />
<div id="strength">{{ passwordStrength }}</div>
On peut aussi spécifier une prop type (pour avoir un input de type différent, comme type="number"),
ou carrément un élément différent avec la prop as (par exemple, :as="textarea"). Si tu veux
afficher du HTML custom, peut-être afin d’ajouter une classe CSS quelque part si le champ est
invalide, tu peux le faire facilement :
[Link]
<Field v-slot="{ field, meta }" v-model="[Link]" name="name" rules="required">
<label for="name-input" :class="{ 'text-danger': [Link] && ![Link] }">
Name</label>
<input id="name-input" :class="{ 'is-invalid': [Link] && ![Link] }" v-
bind="field" />
</Field>
185
Ce type de composant qui projette du contenu avec un slot est celui que nous avons déjà rencontré
dans le chapitre Slots . Il est parfois appelé Higher Order Component, un composant de plus haut
ordre.
Field expose une slot prop, contenant toutes les propriétés que useField() renvoie.
On peut accéder à la propriété meta comme tu peux le voir dans l’exemple précédent, mais aussi à
d’autres propriétés comme errorMessage, qui contient le potentiel message d’erreur du champ. On
peut même faire un peu mieux pour afficher ce message, car VeeValidate offre un troisième
composant : ErrorMessage.
[Link]
<Field v-slot="{ field, meta }" v-model="[Link]" name="name" rules="required">
<label for="name-input" :class="{ 'text-danger': [Link] && ![Link] }">
Name</label>
<input id="name-input" :class="{ 'is-invalid': [Link] && ![Link] }" v-
bind="field" />
<ErrorMessage name="name" class="error" />
</Field>
VeeValidate est suffisamment malin pour ne générer ce message que lorsque l’utilisateur a
effectivement interagi avec le champ, donc il ne crie pas sur le pauvre utilisateur qui viendrait
d’atterrir sur la page.
Le message affiché ici est name is not valid., mais il peut être customisé lorsque l’on charge la
règle de validation, en utilisant le package @vee-validate/i18n :
[Link]
import { configure } from 'vee-validate';
import { localize } from '@vee-validate/i18n';
configure({
validateOnInput: true,
generateMessage: localize('en', {
messages: {
// use a function
required: context => `The ${[Link]} is required.`,
confirmed: context => `The ${[Link]} does not match.`,
// or use the special syntax offered by the library
min: 'The {field} must be at least 0:{min} characters.',
}
})
});
Le message est maintenant The name is required..
Tu peux aussi renommer un champ pour un joli message d’erreur, avec la prop label :
186
[Link]
<Field
v-slot="{ field, meta }"
v-model="[Link]"
name="confirmPassword"
rules="required|confirmed:@password"
label="password confirmation"
>
<label for="confirm-password-input" :class="{ 'text-danger': [Link] &&
![Link] }">Confirm password</label>
<input
id="confirm-password-input"
type="password"
:class="{ 'is-invalid': [Link] && ![Link] }"
v-bind="field"
/>
<ErrorMessage name="confirmPassword" class="error" />
</Field>
Note que l’on peut bien sûr appliquer plusieurs règles d’un coup à un champ :
[Link]
<Field v-slot="{ field, meta }" v-model="[Link]" name="password"
rules="required|min:3">
<label for="password-input" :class="{ 'text-danger': [Link] && ![Link] }"
>Password</label>
<input id="password-input" type="password" :class="{ 'is-invalid': [Link] &&
![Link] }" v-bind="field" />
<ErrorMessage name="password" class="error" />
</Field>
Form a aussi une slot prop avec différentes propriétés, comme meta qui contient l’état du formulaire
(valid, dirty, etc.).
[Link]
<Form v-slot="{ meta: formMeta }" @submit="register($event)">
<!-- Several fields... -->
<button :disabled="![Link]">Register</button>
</Form>
Un troisième composant, FieldArray est également disponible pour gérer des tableaux de valeurs.
Pour terminer, sache que VeeValidate permet aussi de traduire les messages d’erreur dans la langue
voulue, de définir ses propres règles de validation, d’ajouter des attributs d’accessibilité aux
champs, etc.
VeeValidate vient avec un plugin pour les devtools, ce qui permet de directement
187
inspecter l’état de ton formulaire dans les devtools de ton navigateur pendant que
tu développes *.
Essaie notre exercice Login ! Il fait partie du Pro Pack, et tu y apprendras à
construire un formulaire complet validé par VeeValidate.
22.2.3. Validateurs custom
VeeValidate nous met à disposition un ensemble de règles, mais il est également possible de
construire les nôtres.
Une règle de validation est assez simple : c’est une fonction qui renvoie true si la valeur est valide,
ou false sinon. Il est aussi possible de retourner directement le message d’erreur, mais j’utilise
généralement seulement un booléen comme type de retour. La fonction peut également renvoyer
une Promise si tu as besoin de validation asynchrone (par exemple pour vérifier une valeur auprès
du serveur).
Essayons donc de construire un validateur qui vérifie si une valeur est entre 18 et 130.
[Link]
export function between18And130(value: string) {
return +value >= 18 && +value <= 130;
}
Tu dois ensuite ajouter la règle, comme on le fait avec les règles fournies.
[Link]
defineRule('between18And130', between18And130);
Et définir un message pour cette erreur :
[Link]
between18And130: context => `The ${[Link]} must be between 18 and 130.`,
On peut alors l’utiliser dans un composant :
[Link]
<Field v-slot="{ field, meta }" name="age" rules="required|between18And130">
Un validateur peut aussi avoir des paramètres. Au lieu de coder en dur les valeurs minimum et
maximum, nous pouvons les passer au validateur.
188
[Link]
export function betweenParams(value: string, params: Array<string>) {
const min = +params[0]!;
const max = +params[1]!;
return +value >= min && +value <= max;
}
On passe des paramètres en utilisant :, et , pour les séparer.
[Link]
<Field v-slot="{ field, meta }" name="age" rules="required|betweenParams:17,120">
Le message d’erreur peut également refléter ces paramètres :
[Link]
betweenParams: context => {
const params = [Link]!.params as Array<string>;
return `The ${[Link]} must be between ${params[0]} and ${params[1]}.`;
}
Une autre fonctionnalité assez cool est la possibilité de faire de la validation entre champs. Par
exemple, notre validateur peut utiliser les valeurs définies dans deux autres champs pour les
valeurs minimum et maximum.
Si le formulaire a des champs min et max, on peut écrire :
[Link]
<Field v-slot="{ field, meta }" name="age" rules="required|betweenParams:@min,@max">
La syntaxe @something indique à VeeValidate qu’il faut utiliser la valeur du champ nommé
something.
Pour résumer, il est assez facile de créer nos propres validateurs. On écrit une fonction qui renvoie
vrai ou faux, on l’ajoute, on définit un message d’erreur, et on l’utilise, comme un validateur fourni
par défaut.
Essaie notre exercice Validateurs custom ! Il fait partie du Pro Pack, et tu y
apprendras à créer tes propres validateurs.
22.3. Composants de formulaire personnalisés
HTML définit un nombre important de contrôles standards : texte, mot de passe, case à cocher, etc.
Mais, parfois, ces contrôles standards ne conviennent pas.
189
Vue permet de définir des composants bien sûr, et il est en fait possible d’en faire des éléments de
formulaires, c’est-à-dire de les lier en leur appliquant la directive v-model.
Remplir le contrat de cette directive est assez simple. Il faut :
• accepter une prop appelée modelValue;
• notifier Vue que l’utilisateur a changé la valeur, en émettant un événement nommé
@update:modelValue;
Nous allons illustrer tout cela en utilisant un composant rating. Ce composant permet de donner
une note de 0 à 5 à un film, par exemple. Mais au lieu d’utiliser un champ de type number ou range,
on voudrait que l’utilisateur clique simplement sur un bouton parmi 6 boutons affichés (qui,
typiquement, seraient présentés sous forme d’étoiles, mais nous laisserons ça de côté dans
l’exemple qui suit).
Voici le code d’un tel composant :
<script setup lang="ts">
defineProps<{
modelValue: number;
}>();
const emit = defineEmits<{
'update:modelValue': [value: number];
}>();
const pickableValues = [0, 1, 2, 3, 4, 5];
function setValue(pickedValue: number) {
emit('update:modelValue', pickedValue);
}
</script>
Et voici son template :
<template>
<div>
<button
v-for="pickableValue of pickableValues"
:key="pickableValue"
:class="{ selected: modelValue != null && pickableValue <= modelValue }"
type="button"
@click="setValue(pickableValue)"
>
{{ pickableValue }}
</button>
</div>
</template>
190
Et voilà. Nous disposons à présent d’un joli composant qui peut être utilisé dans n’importe quel
formulaire, en lui appliquant simplement la directive v-model :
<form>
<div>
<label for="title">Title</label>
<input id="title" v-model="movieTitle" />
</div>
<div>
<label for="rating">Rating</label>
<Rating id="rating" v-model="movieRating" />
</div>
22.4. Macro defineModel
Quand tu as un composant de formulaire personnalisé qui doit seulement lier une valeur de v-
model à un input classique, la mécanique de prop/event est parfois un peu pénible :
<template>
<input :value="modelValue" @input="setValue(($[Link] as
HTMLInputElement).value)" />
</template>
<script setup lang="ts">
defineProps<{ modelValue: string }>();
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();
function setValue(newValue: string) {
emit('update:modelValue', newValue);
}
</script>
Depuis Vue v3.3, il est possible de simplifier ce composant, en utilisant la macro defineModel :
<template>
<input v-model="modelValue" />
</template>
<script setup lang="ts">
const modelValue = defineModel<string>();
</script>
defineModel accepte également quelques options :
• required: true indique que la prop est obligatoire
• default: value permet de spécifier la valeur par défaut
191
Il est également possible de gérer les modifiers depuis la v3.4 :
<template>
<input v-model="count" />
</template>
<script setup lang="ts">
const [count, countModifiers] = defineModel<number, 'number'>({
set(value) {
if (countModifiers?.number) {
return Number(value);
}
return value;
}
});
</script>
192