Les fomulaires multi-étapes permettent de récupérer de nombreuses informations sur l'utilisateur sans l'assomer d'un coup avec une grande quantité d'informations. Pour les besoins d'un projet, je me suis penché sur la réalisation d'un formulaire multi-étapes (ou multi-steps en anglais) sous Drupal.
Je recherchais une solution permettant d'afficher une barre de progression, un nombre variables d'étapes, un comportement en ajax, et la possibilité d'accéder facilement aux données utilisateur saisies. Des solutions envisagées j'ai retenu le module mforms. Le résultat de son implémentation est visible ici.
Retour sur expérience.
Un module complet
Installation & exemple
On commence par récupérer le module.
drush dl mforms -y
Une fois téléchargé, le module mforms est livré avec un module d'exemple complet que je vous recommande d'activer.
drush en mforms,mforms_example -y
Au moment de l'écriture de cet article, il existe un bug dans le module d'exemple concernant la gestion de plusieurs formulaires mforms sur la même page. Si vous rencontrez le soucis, utilisez le module developpement.
Un module, deux approches
Si vous avez activé le module d'exemple, vous pouvez vous rendre sur la page <votredrupal>/mforms pour une page d'exemple de mise en place de votre module.
Le module mforms propose deux façons pour gérer le contenu de vos formulaires multi-étapes :
- Un stockage dans la variable $form_state ( variable Drupal de stockage des champs de formulaires )
- Un stockage des données dans une varriable de SESSION.
Ces deux approches diffèrent principalement au niveau de la persistance des variables.
En effet les variables stockées avec $form_state seront rafraichies au chargement de la page, l'utilisation de $form_state est à privilégier pour les formulaires multi-étapes "simple" ou la deuxième étapes du fomulaire est une étape de validation de données (par exemple).
À contrario, l'utilisation d'une variable de SESSION va permettre de réutiliser les données saisies par l'utilisateur sur l'ensemble du site Internet mais également de revenir sur les saisies effectuées. C'est cette approche que j'ai retenu pour mon projet.
Construction d'un formulaire multi-étapes
Notre formulaire multi-étapes va faire l'objet d'un module personnalisé. Pour cet exemple je vais appeler le module multi_etapes. Comme nous allons traiter de la réalisation d'un module Drupal, le code sera commenté en anglais pour être en accord avec les bonnes pratiques Drupal. Si certains bouts de codes ne sont pas clairs, n'hésitez pas à poser vos questions en commentaire.
Les bases du module
Je ne reviens pas sur les bases d'un module Drupal, tout est bien expliqué sur la documentation officielle.
multi_etapes.info
name = Multi Etapes
description = Add a multi-steps session based form. Built with the mforms module.
core = 7.x
; Package name
package = Alan
; Module dependencies
dependencies[] = mforms
Ensuite le fichier .module :
multi_etapes.module
<?php
/**
* @file
* Add a multi-steps session based form. Built with the mforms module.
*/
/**
* Implements hook_permission().
*/
function multi_etapes_permission() {
return array(
// 'administer my module' => array(
// 'title' => t('Administer my module'),
// 'description' => t('Perform administration tasks for my module.'),
// ),
'access multi-steps forms' => array(
'title' => t('Access multi-steps forms'),
'description' => t('Enable the user to fill-in custom multi-steps forms'),
),
);
}
/**
* Implements hook_menu().
*/
function multi_etapes_menu() {
$items['myform'] = array(
'title' => t('My multi-steps form'),
'page callback' => 'multi_etapes_myform_page',
'access arguments' => array('access multi-steps forms'),
'type' => MENU_NORMAL_ITEM,
'file' => 'inc/multi_etapes.pages.inc',
);
return $items;
}
/**
* Implements hook_STORE_KEY_mforms_init().
*/
function multi_etapes_myform_mforms_init() {
$store = MformsSessionStore::getInstance('myform');
$steps = MformsSteps::getInstance($store);
// If using mustistep controls you need to explicitly define form steps and
// its attributes.
$controls = MformsMultiStepControls::getInstance($store, $steps, array(
'_myform_step1' => array('value' => t('Bio'), 'weight' => -103),
'_myform_step2' => array('value' => t('Hobbies'), 'weight' => -102),
'_myform_step3' => array('value' => t('Summary'), 'weight' => -101)
));
// Ajaxify the form stepping process.
$controls->ajaxify();
// Init the mforms.
mforms_init_module('multi_etapes', 'myform', $store, $controls);
}
Qu'avons-nous fait ?
- On créé un droit pour accéder au formulaire multi-étape,
- On défini une entrée de menu "classique" pour accéder à ce formulaire (protégée par le droit créé précédemment),
- On indique que la fonction de callback "multi_etapes_myform_page" se situe dans le fichier /inc/ du module,
- On initialise notre formulaire multi-étapes où myform est la clef via le hook hook_STORE_KEY_mforms_init(),
- On défini le nombre d'étapes à l'intérieur du hook et on leur donne un nom (attention au poids),
- On fini d'initialiser le formulaire.
inc/multi_etapes.pages.inc
<?php
/**
* Entry page for the multi-step form.
*
* @return array
* Drupal renderable array.
*/
function multi_etapes_myform_page() {
// Add user name to steps
global $user;
$uname = isset($user->name)?$user->name:'Guest';
// Create parameters to be passed to the multi-step form
$params = array('uname' => $uname);
// Return Drupal renderable array.
return array(
'mform' => drupal_get_form('multi_etapes_myform', $params),
);
}
/**
* Callback function to generate form.
*
* @param array $form
* Drupal form array.
* @param array $form_state
* Drupal form_state array.
* @param array $params
* Optional params passed into form.
*
* @return array
* Drupal form array.
*/
function multi_etapes_myform($form, &$form_state, $params) {
// pass defined parameters to mforms_build
return mforms_build('myform', '_myform_step1', $form_state, $params);
}
/**
* Callback function to validate form inputs
*
* @param array $form
* Drupal form array.
* @param array $form_state
* Drupal form_state array.
*/
function multi_etapes_myform_validate($form, &$form_state) {
mforms_validate('myform', $form, $form_state);
}
/**
* Callback function to process the form inputs.
*
* @param array $form
* Drupal form array.
* @param array $form_state
* Drupal form_state array.
*/
function multi_etapes_myform_submit($form, &$form_state) {
mforms_submit('myform', $form, $form_state);
}
Encore beaucoup d'initialisations dans ce fichier :
- On défini la fonction appelée par notre menu,
- On récupère le nom de l'utilisateur courant pour montrer le passage de variable à notre formulaire,
- On appelle le formulaire via drupal_get_form,
- Notre formulaire multi-étapes est défini de façon traditionnelle avec les hook_form , hook_validate et hook_submit.
Il nous reste à mettre en place le code structurant chacune des étapes de notre formulaire.
Le module mform va chercher les étapes ( définies par _myform_stepX() ) dans un fichier qui doit être nommé de la façon suivante : nom_du_module.cleformulaire.inc et placé dans un dossier mforms à la racine de votre module.
Le coeur du fomulaire multi-étapes
Nous allons faire un formulaire d'exemple en 3 étapes :
- Informations sur l'utilisateur avec son login récupéré automatiquement,
- Ses activités (Films, livres et sports),
- Un résumé des valeurs saisies, un champ pour envoyer un message à l'équipe du site et une case a cocher pour valider et terminer le formulaire.
Ce formulaire reprend les éléments du formulaire d'exemple fourni par le module mforms, en ajoutant des variations qui me semblent intéressantes.
mforms/multi_etapes.myform.inc - Première étape
/**
* First step called by the Mforms state machine.
*
* @param array $form_state
* Drupal form_state array.
* @param string $next_step
* Mforms next step callback name.
* @param mixed $params
* Optional params passed into form.
*
* @return array
* Drupal form array.
*/
function _myform_step1(&$form_state, &$next_step, $params) {
// Define following step callback. If none set, that implies it is
// the last step.
$next_step = '_myform_step2';
// Retrieve submitted values. This comes in handy when back action
// occured and we need to display values that were originaly submitted.
$data = mforms_get_vals('myform');
// If we have the data it means we arrived here from back action, so show
// them in form as default vals.
if (!empty($data)) {
$vals = $data;
}
elseif (isset($form_state['values'])) {
$vals = $form_state['values'];
}
// Define form array and return it.
$form = array();
$form['login'] = array(
'#type' => 'textfield',
'#disabled'=>true,
'#title' => t('Login'),
'#default_value' => isset($vals['loginv']) ? $vals['loginv'] : $params['uname'], // get login name from previous page (not submited but for design purpose).
);
$form['name'] = array(
'#type' => 'textfield',
'#title' => t('Name'),
'#default_value' => isset($vals['name']) ? $vals['name'] : NULL,
);
$form['email'] = array(
'#type' => 'textfield',
'#title' => t('Email'),
'#default_value' => isset($vals['email']) ? $vals['email'] : NULL,
'#required' => TRUE,
);
$form['www'] = array(
'#type' => 'textfield',
'#title' => t('Your web site'),
'#default_value' => isset($vals['www']) ? $vals['www'] : NULL,
);
// store the login in a hidden field so that the value is submited
$form['loginv'] = array(
'#type' => 'hidden',
'#default_value' => isset($vals['loginv']) ? $vals['loginv'] : $params['uname'],
);
return $form;
}
/**
* Validate callback - validates email address.
*
* @param array $form
* Drupal form array.
* @param array $form_state
* Drupal form_state array.
*/
function _myform_step1_validate($form, &$form_state) {
if (!valid_email_address($form_state['values']['email'])) {
form_set_error('email', t('Invalid email.'));
}
}
- On défini la fonction de première étape,
- On indique l'étape suivante,
- On défini les champs à afficher tout en peuplant les valeurs présaisies si elles existent,
Pour cet exemple, le champ "Login" est récupéré automatiquement. Si l'utilisateur est anonyme, "Guest" est affiché,
- On soumet la première étape du fomulaire à la fonction de validation.
mforms/multi_etapes.myform.inc - Deuxième étape
/**
* Step two.
*
* @param array $form_state
* Drupal form_state array.
* @param string $next_step
* Mforms next step callback name.
* @param mixed $params
* Optional params passed into form.
*
* @return array
* Drupal form array.
*/
function _myform_step2(&$form_state, &$next_step, $params) {
$next_step = '_myform_step3';
$form = array();
$data = mforms_get_vals('myform');
if (!empty($data)) {
$vals = $data;
}
elseif (isset($form_state['values'])) {
$vals = $form_state['values'];
}
$form['movies'] = array(
'#type' => 'textarea',
'#title' => t('Movies'),
'#default_value' => isset($vals['movies']) ? $vals['movies'] : NULL,
);
$form['books'] = array(
'#type' => 'textarea',
'#title' => t('Books'),
'#default_value' => isset($vals['books']) ? $vals['books'] : NULL,
);
$form['sports'] = array(
'#type' => 'textarea',
'#title' => t('Sports'),
'#default_value' => isset($vals['sports']) ? $vals['sports'] : NULL,
);
return $form;
}
Rien de plus par rapport à l'étape 1, on passe à la dernière étape de ce formulaire.
mforms/multi_etapes.myform.inc - Troisième étape
/**
* Third step.
*
* @param array $form_state
* Drupal form_state array.
* @param string $next_step
* Mforms next step callback name.
* @param mixed $params
* Optional params passed into form.
*
* @return array
* Drupal form array.
*/
function _myform_step3(&$form_state, &$next_step, $params) {
$form = array();
// Get the collected values submited at each step.
// Here is one difference - the second parameter that defines the step
// from which we want to retrieve the data.
$vals1 = mforms_get_vals('myform', '_myform_step1');
$vals2 = mforms_get_vals('myform', '_myform_step2');
$form['summary'] = array(
'#type' => 'fieldset',
'#title' => t('Summary'),
);
$form['summary']['sum_bio'] = array(
'#markup'=>
"<h3>".t('Bio')."</h3>
<ul>
<li><em>".t('Login')." :</em> ${vals1['loginv']}</li>
<li><em>".t('Name')." :</em> ${vals1['name']}</li>
<li><em>".t('e-mail')." :</em> ${vals1['email']}</li>
<li><em>".t('Website')." :</em> ${vals1['www']}</li>
</ul>",
);
$form['summary']['sum_hobbies'] = array(
'#markup'=>
"<h3>".t('Hobbies')."</h3>
<ul>
<li><em>".t('Movies')." :</em> ${vals2['movies']}</li>
<li><em>".t('Books')." :</em> ${vals2['books']}</li>
<li><em>".t('Sports')." :</em> ${vals2['sports']}</li>
</ul>",
);
$data = mforms_get_vals('myform');
if (!empty($data)) {
$vals = $data;
}
elseif (isset($form_state['values'])) {
$vals = $form_state['values'];
}
$form['message_team'] = array(
'#type' => 'textarea',
'#title' => t('A message for the team'),
'#default_value' => isset($vals['message_team']) ? $vals['message_team'] : NULL,
);
$form['confirm']=array(
'#type' => 'checkbox',
'#title' => t('Validate and end the survey'),
);
return $form;
}
/**
* Validate callback.
*
* @param array $form
* Drupal form array.
* @param array $form_state
* Drupal form_state array.
*/
function _myform_step3_validate($form, &$form_state) {
if (!$form_state['values']['confirm']) {
form_set_error('confirm', t('You have to validate the survey in order to continue !'));
}
}
/**
* Implement submit callback for the last step to process all data submitted.
*
* @param array $form
* Drupal form array.
* @param array $form_state
* Drupal form_state array.
*/
function _myform_step3_submit($form, &$form_state) {
// Here do what you want with the data
multi_etapes_save_data() // exemple function
// Send a mail to the team if there is a custom message
if($form_state['values']['message_team'] && !empty($form_state['values']['message_team'])){
// add your own mail function
if(!_multi_etapes_drupal_mail('contact@votresite.com','webmaster@votresite.com',t('Answer to the survey'),$form_state['values']['message_team'])){
drupal_set_message(t('Something went wrong'),'error');
}
}
// Call mforms_clean();
// Clear all data from the session variable
mforms_clean('myform');
drupal_set_message(t('Thank you for your time.'));
}
- Cette fois on affiche un résumé des information saisies précédemment, on utilise la fonction mforms_get_vals avec un second paramètre pour récupérer les valeurs d'une étape en particulier,
- On affiche les résultats des deux étapes précédentes,
- On affiche un champ pour que l'utilisateur puisse envoyer un message à l'équipe du site,
- On met en place une case à cocher pour valider nos résultats et envoyer le message (si saisi),
- Une fonction de validation vérifie que la case a bien été cochée lors de l'envoi du formulaire,
- La fonction d'envoi traite les données (je ne développe pas ce point, mais on peut imaginer que les données saisies viennent peupler une node ou une entitée personnalisée),
- Si l'utilisateur a entré un message à destination de l'équipe on envoi ce message par mail (par exemple),
- On vide le contenu de notre formulaire (effacement de la variable de SESSION) et on affiche un message à l'utilisateur.
Et voilà ! Notre module est en place et fonctionnel. Les possibilités sont nombreuses et le système robuste permet de créer n'importe quel type de formulaire multi-étape.
Pour aller plus loin
Quelques astuces complémentaires.
Execution de javascript
Lors de la réalisation du formulaire multi-étape pour le site applicatif de l'eGrid, j'ai eu besoin d'executer du code javascript pour réaliser un carrousel (étape Évaluation).
Le problème avec l'execution de javascript dans les appels en ajax Drupal, c'est qu'il est toujours délicat de savoir à quel moment se "brancher" pour executer son code. Le morceau de javascript suivant permet d'executer le code souhaité à l'étape désirée. Attention cependant le code est réinterprété a chaque reconstruction du DOM (et donc à chaque affichage de message d'erreur de validation).
J'ai placé le code dans js/multi_etapes.js et modifié multi_etapes.info pour déclarer le fichier js ( scripts[] = js/multi_etapes.js ).
(function ($, Drupal, window, document, undefined) {
Drupal.behaviors.multi_etapes = {
attach: function(context, settings) {
if(context === document || context[0].nodeName == 'FORM'){
var step = $('[id^="edit-steps-btns-wrapper"] .current-step').attr('id');
var regex = /step(\d)/;
var match = regex.exec(step);
step = parseInt(match[1]);
alert('Etape ' + step);
// on peut envisager un switch pour gérer chaque étape.
}
}
}
})(jQuery, Drupal, this, this.document);
Si vous vous aventurez dans la réalisation d'un tel formulaire avec javascript, je serai très interessé par vos retours et vos solutions pour bien gérer l'execution JS.
Accéder aux données de la session
Le fait de travailler avec les sessions permet d'accéder à tout moment aux valeurs du formulaire. Ceci m'a été particulièrement utile lors de la génération d'une fiche de résumé des saisies en PDF. Pour accéder aux valeurs stockées :
$valeurs = $_SESSION['clefduformulaire']
Ce sera tout pour aujourd'hui concernant les formulaires multi-étapes sous Drupal, il existe d'autres méthodes utilisant le module webform ou en construisant directement avec l'API form de Drupal, avez-vous essayé ces autres méthodes ? Qu'en pensez-vous ?