Construire un formulaire en plusieurs étapes

Plugin officiel Multi-Step

À partir de 1.0.0-beta.15, FormKit inclut un plugin officiel de première partie qui crée un type d'entrée multi-step.

Bien qu'il soit toujours utile de comprendre comment construire une entrée en plusieurs étapes par vous-même — si vous cherchez le moyen le plus simple d'utiliser une entrée en plusieurs étapes dans votre projet, consultez le plugin officiel FormKit multi-step — il est gratuit et open-source !

Peu d'interactions sur le web sont aussi désagréables que de se retrouver face à un grand formulaire intimidant. Les formulaires en plusieurs étapes — parfois appelés "wizards" — peuvent atténuer cette douleur en divisant un grand formulaire en petites étapes plus abordables — mais ils peuvent aussi être compliqués à construire.

Dans ce guide, nous allons vous montrer comment construire un formulaire en plusieurs étapes avec FormKit et comment nous pouvons offrir une expérience utilisateur supérieure avec un minimum de code. Commençons !

Composition API

Ce guide suppose que vous êtes familiarisé avec la Vue Composition API.

Exigences

Commençons par définir les exigences de notre formulaire en plusieurs parties :

  • Montrer à l'utilisateur à quelle étape il se trouve par rapport à toutes les étapes requises.
  • Permettre à l'utilisateur de naviguer à volonté à n'importe quelle étape du formulaire.
  • Montrer immédiatement si chaque étape a passé toutes les validations frontend ✅.
  • Agréger les données du formulaire de toutes les étapes en un seul objet pour la soumission.
  • Afficher les erreurs de backend retournées sur les champs appropriés et également à l'étape appropriée.

Création d'un formulaire basique

D'abord, créons un formulaire basique sans étapes pour avoir du contenu avec lequel travailler. Notre exemple sera une fausse demande pour recevoir une subvention, nous organiserons donc le formulaire en 3 sections — "Informations de contact", "Informations sur l'organisation" et "Demande". Ces sections deviendront les étapes complètes du formulaire plus tard.

Nous inclurons un mélange de règles de validation pour chaque entrée, et nous limiterons chaque section à une question pour l'instant jusqu'à ce que nous ayons la structure complète en place. Enfin, pour les besoins de ce guide, nous afficherons les données du formulaire collectées en bas de chaque exemple :

Charger l'exemple en direct

Diviser le formulaire en sections

Maintenant que nous avons une structure définie, divisons le formulaire en sections distinctes.

Pour commencer, enveloppons chaque section d'entrées avec un groupe (<FormKit type="group" />) afin que nous puissions valider chaque groupe indépendamment. Les groupes FormKit sont puissants car ils sont conscients de l'état de validation de tous leurs descendants sans affecter le balisage de votre formulaire.

Un groupe lui-même devient valide lorsque tous ses enfants (et leurs enfants) sont valides :

<!-- Seul un groupe est montré ici pour plus de brièveté -->
<FormKit type="group" name: "contactInfo">
  <FormKit type="email" label="*Adresse e-mail" validation="required|email" />
</FormKit>
...

Dans notre cas, nous allons également vouloir un balisage HTML enveloppant. Mettons chaque groupe dans une section "étape" que nous pouvons montrer et cacher conditionnellement :

<!-- Seul un groupe est montré ici pour plus de brièveté -->
<section v-show="step === 'contactInfo'">
  <FormKit type="group" name: "contactInfo">
    <FormKit type="email" label="*Adresse e-mail" validation="required|email" />
  </FormKit>
</section>
...

Ensuite, introduisons une interface utilisateur de navigation pour que nous puissions basculer entre chaque étape :

// pour l'instant, définissons manuellement les noms des étapes
const stepNames = ['contactInfo', 'organizationInfo', 'application']
<!-- Mettre en place une interface utilisateur de navigation par onglets. Au clic, changer d'étape -->
<ul class="steps">
  <li
    v-for="stepName in stepNames"
    class="step"
    @click="step = stepName"
    :data-step-active="step === stepName"
  >
    {{ camel2title(panel) }}
  </li>
</ul>

Voici à quoi cela ressemble une fois assemblé :

Styles non inclus

Les CSS pour les formulaires en plusieurs étapes — comme les onglets dans cet exemple — ne sont pas inclus dans le thème par défaut Genesis. Les styles ont été écrits sur mesure pour cet exemple et vous devrez fournir les vôtres.

Charger l'exemple en direct

Ça commence à ressembler à un vrai formulaire en plusieurs étapes ! Il reste cependant du travail à faire car nous avons quelques problèmes :

  • La validité de chaque étape individuelle n'est pas montrée.
  • Lorsqu'il y a des validations sur un onglet qui n'est pas l'"étape actuelle", elles ne peuvent pas être vues.

Réglons le premier problème.

Suivi de la validité pour chaque étape

FormKit suit déjà la validité du group par défaut. Nous devrons simplement capturer ces données pour pouvoir les utiliser dans notre interface utilisateur.

Un concept important à retenir à propos de FormKit est que chaque composant <FormKit> a un noeud central correspondant, qui a lui-même un objet node.context réactif. Cet objet context suit la validité du noeud dans context.state.valid. Comme mentionné ci-dessus, un group devient valide lorsque tous ses descendants sont valides. Avec cela à l'esprit, construisons un objet qui stocke la validité réactive de chacun des groupes.

Nous allons utiliser la fonctionnalité plugin de FormKit pour faire ce travail. Bien que le terme "plugin" puisse sembler intimidant, les plugins dans FormKit ne sont que des fonctions de configuration qui sont appelées lorsqu'un noeud est créé. Les plugins sont hérités par tous les descendants (comme les enfants au sein d'un groupe).

Voici notre plugin personnalisé, appelé stepPlugin :

// notre plugin et notre code de modèle feront usage de 'steps'
const steps = reactive({})

const stepPlugin = (node) => {
  // ne fonctionne que pour <FormKit type="group" />
  if (node.props.type == 'group') {
    // construire notre objet steps
    steps[node.name] = steps[node.name] || {}

    // ajouter la validité réactive du groupe actuel
    node.on('created', () => {
      steps[node.name].valid = toRef(node.context.state, 'valid')
    })

    // Arrêter l'héritage du plugin aux noeuds descendants.
    // Nous ne nous soucions que des groupes de niveau supérieur
    // qui représentent les étapes.
    return false
  }
}

L'objet réactif steps résultant de notre plugin ci-dessus ressemble à ceci :

{
  contactInfo: { valid: false },
  organizationInfo: { valid: false }
  application: { valid: false }
}

Pour utiliser notre plugin, nous l'ajouterons à notre formulaire racine <FormKit type="form" />. Cela signifie que chaque groupe de niveau supérieur dans notre formulaire héritera du plugin :

<FormKit type="form" :plugins="[stepPlugin]"> ... reste du formulaire </FormKit>

Affichage de la validité

Maintenant que notre modèle a accès en temps réel à l'état de validité de chaque groupe via notre plugin, écrivons l'interface utilisateur pour montrer ces données dans la barre de navigation des étapes.

Nous n'avons plus besoin de définir manuellement nos étapes puisque notre plugin stocke dynamiquement le nom de tous les groupes dans l'objet steps. Ajoutons un attribut data-step-valid="true" à chaque étape si elle est valide afin que nous puissions la cibler avec CSS :

<ul class="steps">
  <li
    v-for="(step, stepName) in steps"
    class="step"
    @click="activeStep = stepName"
    :data-step-valid="step.valid"
    :data-step-active="activeStep === stepName"
  >
    {{ camel2title(stepName) }}
  </li>
</ul>

Avec ces mises à jour, notre formulaire est maintenant capable d'informer un utilisateur lorsqu'ils ont correctement rempli tous les champs d'une étape donnée !

Nous apporterons également quelques autres améliorations :

  • Extraire la "logique des étapes" dans un composable Vue afin qu'elle puisse être réutilisée ailleurs.
  • Créer un fichier utils.js pour nos fonctions utilitaires.
  • Définir la 1ère étape que nous trouvons comme activeStep.
Charger l'exemple en direct

Affichage des erreurs

L'affichage des erreurs est plus nuancé. Bien que l'utilisateur ne le sache peut-être pas, il existe en réalité 2 types d'erreurs que nous devons gérer et communiquer à l'utilisateur :

  • Les erreurs provenant de l'échec des règles de validation frontend (messages de type validation)
  • Les erreurs backend (messages de type error)

FormKit utilise son magasin de messages pour suivre ces deux types d'erreurs/messages.

Avec notre plugin déjà en place, il est relativement simple d'ajouter un suivi pour les deux :

const stepPlugin = (node) => {
  ...
  // Stocker ou mettre à jour le nombre de messages de validation bloquants.
  // FormKit émet l'événement "count:blocking" (avec le compte) chaque fois
  // que le compte change.
  node.on('count:blocking', ({ payload: count }) => {
    steps[node.name].blockingCount = count
  })

  // Stocker ou mettre à jour le nombre de messages d'erreur backend.
  node.on('count:errors', ({ payload: count }) => {
    steps[node.name].errorCount = count
  })
  ...
}
Messages de validation bloquants vs erreurs

FormKit fait une distinction entre les messages de validation frontend (messages de type validation), et les erreurs (messages de type error).

Mettons à jour notre exemple pour montrer les deux types d'erreurs avec les exigences suivantes :

  • Nous montrerons toujours le nombre d'erreurs backend s'il en existe.
  • Nous ne montrerons le nombre d'erreurs de validation frontend que si l'utilisateur visite puis quitte (floute) un groupe - car nous ne voulons pas les confronter à une interface utilisateur d'erreur s'ils sont encore en cours de progression.

Ajout d'un événement de flou de groupe

Comme "flouter un groupe" n'existe pas en HTML, nous l'introduirons dans notre plugin avec un tableau appelé visitedSteps. Voici le code pertinent :

import { watch } from 'vue'
import { getNode, createMessage } from '@formkit/core'

const stepPlugin = (node) => {
  ...
  const activeStep = ref('')
  const visitedSteps = ref([]) // suivre les étapes visitées

  // Surveillez notre activeStep et stockez les étapes visitées
  watch(activeStep, (newStep, oldStep) => {
    if (oldStep && !visitedSteps.value.includes(oldStep)) {
      visitedSteps.value.push(oldStep)
    }
    // Déclencher l'affichage de la validation sur les champs si un groupe a été visité
    visitedSteps.value.forEach((step) => {
      const node = getNode(step)

      // la méthode node.walk() parcourt tous les descendants du nœud actuel
      // et exécute la fonction fournie.
      node.walk((n) => {
        n.store.set(
          createMessage({
            key: 'submitted',
            value: true,
            visible: false
          })
        )
      })
    })
  })
  ...
}

Vous vous demandez peut-être pourquoi nous parcourons tous les descendants d'une étape donnée (node.walk()) et créons des messages avec une clé de submitted et une valeur de true ? Lorsqu'un utilisateur tente de soumettre un formulaire, c'est ainsi que FormKit s'informe que toutes les entrées sont dans un état submitted. Dans cet état, FormKit force l'apparition de tous les messages de validation bloquants. Nous déclenchons manuellement la même chose dans notre événement "flou de groupe".

L'interface utilisateur d'erreur

Nous utiliserons la même interface utilisateur pour les deux types d'erreurs car les utilisateurs finaux ne se soucient pas vraiment de la distinction. Voici notre HTML de l'étape mise à jour, qui affiche une bulle rouge avec le total cumulé des erreurs errorCount + blockingCount :

<li v-for="(step, stepName) in steps" class="step" ...>
  <span
    v-if="checkStepValidity(stepName)"
    class="step--errors"
    v-text="step.errorCount + step.blockingCount"
  />
  {{ camel2title(stepName) }}
</li>

Nous sommes presque à la ligne d'arrivée ! Voici notre formulaire actuel - qui peut maintenant indiquer à un utilisateur quand il a correctement ou incorrectement rempli chaque étape :

Charger l'exemple en direct

Soumission du formulaire et réception des erreurs

La dernière pièce du puzzle est la soumission du formulaire et la gestion des erreurs que nous recevons du serveur backend. Nous simulerons le backend pour les besoins de ce guide.

Nous soumettons le formulaire en ajoutant un gestionnaire @submit à <FormKit type="form"> :

<FormKit type="form" @submit="submitApp"> ... reste du formulaire</FormKit>

Et voici notre gestionnaire de soumission :

const submitApp = async (formData, node) => {
  try {
    const res = await axios.post(formData)
    node.clearErrors()
    alert('Votre demande a été soumise avec succès !')
  } catch (err) {
    node.setErrors(err.formErrors, err.fieldErrors)
  }
}

Notez que FormKit passe à notre gestionnaire de soumission 2 arguments utiles : les données du formulaire dans un seul objet prêt à être demandé (que nous appelons formData), et le node de base du formulaire, que nous pouvons utiliser pour effacer les erreurs ou définir les erreurs retournées en utilisant les aides node.clearErrors() et node.setErrors(), respectivement.

setErrors() prend 2 arguments : les erreurs au niveau du formulaire et les erreurs spécifiques au champ. Notre faux backend renvoie la réponse err que nous utilisons pour définir les erreurs.

Alors, que se passe-t-il si l'utilisateur est à l'étape 3 (Application) lorsqu'il soumet, et qu'il y a des erreurs au niveau du champ sur une étape cachée ? Heureusement, tant que les nœuds existent dans le DOM, FormKit est capable de placer ces erreurs de manière appropriée. C'est pourquoi nous avons utilisé un v-show pour les étapes au lieu de v-if - Le nœud DOM doit exister pour avoir des erreurs définies sur le nœud FormKit correspondant.

Tout assembler

Et Voilà! 🎉 Nous avons terminé! En plus de notre gestionnaire de soumission, nous avons ajouté quelques autres embellissements UI et UX à ce formulaire final pour le rendre plus réel :

  • Ajout de boutons Précédent / Suivant pour la navigation par étapes.
  • Ajout d'un faux backend à utils.js qui renvoie des erreurs.
  • Le bouton de soumission du formulaire est maintenant désactivé jusqu'à ce que le formulaire entier soit dans un état valid.
  • Ajout de texte supplémentaire au formulaire pour mieux simuler une interface utilisateur réelle.

Le voici — un formulaire multi-étapes entièrement fonctionnel :

Charger l'exemple en direct
Vous voulez le voir construit en utilisant FormKit Schema?Découvrez le Playground

Façons d'améliorer

Bien sûr, il y a toujours des moyens d'améliorer quoi que ce soit, et ce formulaire ne fait pas exception. Voici quelques idées :

  • Sauvegarder l'état du formulaire dans window.localStorage afin que l'état du formulaire d'un utilisateur soit conservé même s'il quitte accidentellement.
  • Pré-remplir toutes les valeurs de formulaire connues afin que l'utilisateur n'ait pas à remplir les données connues.
  • Ajouter un indicateur de statut "pas encore soumis" pour avertir l'utilisateur qu'il doit encore soumettre.

Nous avons couvert beaucoup de sujets dans ce guide et espérons que vous en avez appris davantage sur FormKit et comment l'utiliser pour faciliter les formulaires multi-étapes!

Vous voulez utiliser une entrée multi-étapes dans votre projet?Essayez le plugin officiel