Découvrez nos offres pour faire du digital le levier de votre croissance !
Téléchargez le Guide Ultime de gestion de projet digitale pour vous aider à piloter vos transformations et faire les bons choix !
Les formulaires, c’est pas la vie, mais presque. Cet outil, devenu indispensable, nous permet de récupérer les données de l'utilisateur et ainsi d'interagir avec lui si besoin. Nous, êtres humains, nous avons les questions, le Web, lui, a ses formulaires.
Cela étant, les formulaires peuvent être la hantise des développeurs. Bien que pas forcément compliqué en soi, sa structure et sa gestion comportent des subtilités et peuvent demander un certain temps de mise en place. Et c'est pourquoi Drupal propose la Form API.
En quoi consiste la Form API sur Drupal ? Découvrons-le ensemble…
Cela peut paraître trivial, mais, dans quel cas est-il intéressant d'utiliser la Form API ? La réponse : dès que vous avez un formulaire.
Cela peut être : un bouton d'inscription à une newsletter, un formulaire de contact personnalisé, un formulaire multi-étapes, ..., mais également pour gérer des filtres associées à des pages de listes gérés « programmatiquement », pensez-y ! 😜
Aussi, vous est-il déjà arrivé de modifier un formulaire existant via le hook_form_alter ? Par exemple, pour masquer ou rendre non modifiable un champs d'un formulaire du BO. Et bien, vous avez déjà utilisé Form API !
C'est une API qui met à votre disposition un ensemble d'outils, de méthodes, d'interfaces, permettant de mettre en place assez facilement un formulaire fonctionnel.
En bref, elle vous permet, via les render array, de générer l'HTML d'un formulaire, d'ajouter simplement quels sont les champs requis, de créer un token de sécurité ou encore de générer un objet spécifique qui contiendra les réponses pour la validation et le traitement des données.
Nous allons donc voir ensemble, comment peut-on se servir de la Form API pour créer un formulaire, gérer sa validation et ses erreurs et comment y insérer de l'AJAX.
Comme je le disais précédemment, la Form API se sert des render array de la Render API pour générer un formulaire. Cela signifie que nous allons devoir fournir à Drupal un tableau possédant une structure particulière, qu'il va ensuite interpréter afin de générer le formulaire souhaité.
Pour commencer, je vais créer une classe. Cette classe héritera de la classe FormBase et devra implémenter ces trois méthodes :
getFormId()
Doit renvoyer l'id du formulaire, à définir donc ici.buildForm(array $form, FormStateInterface $form_state)
C'est ici que la notion de render array est importante, car c'est au sein de cette méthode que nous allons construire ce fameux tableau et le renvoyer pour être affiché.submitForm(array &$form, FormStateInterface $form_state)
C'est par cette méthode que nous allons définir le comportement du formulaire à sa soumission. À noter que nous rentrons dans cette fonction uniquement si le formulaire a été validé avec succès. Nous reparlerons de ce dernier point dans la section "Validation".
class RegisterForm extends FormBase {
const FIELD_EMAIL = 'email';
const FIELD_PASSWORD = 'password';
public function getFormId(): string {
return 'register_collaborator';
}
public function buildForm(array $form, FormStateInterface $form_state) {
$form[self::FIELD_EMAIL] = [
'#type' => 'email',
'#title' => 'Votre mail',
];
$form[self::FIELD_PASSWORD] = [
'#type' => 'password_confirm',
'#title' => 'Votre mot de passe'
];
$form['submit'] = [
'#type' => 'submit',
'#value' => "S'enregistrer",
'#weight' => 999
];
return $form;
}
public function submitForm(array &$form, FormStateInterface $form_state) {
$user = User::create();
$user->setPassword($form_state->getValue(self::FIELD_PASSWORD));
$user->enforceIsNew();
$user->setEmail($form_state->getValue(self::FIELD_EMAIL));
$user->setUsername($form_state->getValue(self::FIELD_EMAIL));
$user->save();
return $user;
}
}
Pour afficher (et donc créer) le formulaire, deux solutions s'offrent à vous :
example.form:
path: '/example-form'
defaults:
_title: 'Example form'
_form: '\\Drupal\\example\\Form\\ExampleForm'
requirements:
_permission: 'access content'
$form = \\Drupal::formBuilder()->getForm('Drupal\\example\\Form\\ExampleForm');
Un des avantages de la Form Api est qu’à la création (buildForm), à la validation (validateForm) ainsi qu’à la soumission du formulaire (submitForm), nous avons accès à un paramètre de type FormStateInterface.
Ce paramètre est un objet qui possède l’état du formulaire. Il contient donc de nombreuses informations sur celui-ci et notamment toutes les valeurs qui ont été saisies par l’utilisateur.
Ainsi par exemple au sein de la méthode submit, il sera possible de récupérer ces valeurs, les envoyer à son Manager et éventuellement décider de la route vers laquelle l'utilisateur sera redirigé en fonction de ces mêmes valeurs.
Maintenant, passons à la pratique. Je vous propose de développer 5 étapes : la création, la validation, la soumission, la gestion de l'affichage et je finirai par la gestion des champs en AJAX.
Un formulaire est composé d'un ensemble de champs input, par lesquels les données vont être saisies puis envoyées. La form API propose un certain nombre de FormElements qui sont ensuite convertis en balises HTML.
Ici, j'ai envie de vous présenter les principaux formElements, ainsi que quelques fonctionnalités intéressantes qui pourraient vous faire gagner du temps.
On retrouve les inputs standards tels que : Text, Checkbox, Number, Date, Email, Radios & radio, Fieldset & Fieldgroup.
Il est possible de configurer ces formElements afin de les personnaliser. Voici les propriétés que j'ai le plus utilisées.
Voici par exemple, un input pour une date de naissance :
$form["birthday"] = [
'#type' => 'date',
'#title' => "Your birthdate",
'#weight' => 12,
'#required' => TRUE,
'#placeholder' => "Insert here your birthdate",
'#description' => "The date should match this format: mm/dd/yyyy",
'#description_display' => 'before',
'#format' => 'm/d/Y',
'#attributes' => [
'autocomplete' => 'off',
'type' => 'date',
'max' => (new DateTime('now'))->format('Y-m-d'),
],
];
Parfois, la complexité du formulaire peut vous donner envie d'encapsuler les balises dans des <div>, <span>, <p>, etc. Vous vous sentirez alors un peu coincé. Pas de panique, Form API a prévu des outils pour çà : des éléments et des propriétés.
Voici un exemple d'utilisation d'un container :
$form['optin_group'] = [
'#type' => 'container',
'#attributes' => [
'class' => ['optin-wrapper']
],
'#weight' => 0,
'optin_general' => [
'#type' => 'checkbox',
'#title' => 'I accept general conditions',
'#default_value' => 0,
],
];
// On peut également rajouter un élément dedans comme ceci :
$form['optin_group']['optin_sms'] = [
'#type' => 'checkbox',
'#title' => 'I accept to be contacted by SMS',
#default_value' => 0,
];
Avant de parler de la validation coté serveur, il est bon de rappeler qu'il est possible de mettre en place, simplement, une validation coté client :
Par exemple, voici comment construire un champs obligatoire pour un numéro de téléphone :
$form["phone"] = [
'#type' => 'number',
'#title' => 'Your phone number',
'#required' => TRUE,
'#attributes' => [
'pattern' => '[0-9]{11}',
'title' => 'Please insert 10 numbers',
]
];
Bien entendu, il est possible de faire davantage de vérification en utilisant JavaScript. Mais bien qu'offrant une excellente expérience utilisateur, ces validations ne sont pas suffisantes en elles-même. En effet, quelqu'un de mal intentionné pourrait facilement contourner ces contrôles en modifiant le DOM à partir du navigateur ou bien en créant une requête HTTP « à la mano ». C'est pourquoi une validation côté serveur est toujours indispensable.
La validation se fait via la méthode validateForm(). Les informations sont récupérées depuis le formstate, puis nous effectuons les vérifications nécessaire aux données récupérées. En cas d'erreur, nous indiquons au formstat la nature de l'erreur ainsi que le champ concerné.
public function validateForm(array &$form, FormStateInterface $form_state) {
$this->validatePhoneNumber($form, $form_state);
}
public function validatePhoneNumber(array $form, FormStateInterface &$form_state) {
$phoneNumberTester = \Drupal::service('my_module.phone_number_tester');
if (!$phoneNumberTester->isValidNumber($form_state->getValue('phone'), 'FR') {
$form_state->setErrorByName('phone', t("Your phone number is not formatted correctly"));
}
}
Une bonne pratique : préférez découper votre fonction de validation en plusieurs sous fonctions, qui auront la charge de valider son propre champs (exemple : validateBirthdate
pour valider la date de naissance). Il s'agit du S du concept SOLID (S pour Single responsability !).
Vous ne connaissez pas encore les 5 principes qui composent le concept SOLID 😱, je vous invite à lire notre article intitulé "Comment devenir un Dev SOLID ?" 😉
Ici, j'utilise la méthode $form_state→setErrorByName qui permet d'attribuer une erreur champ phone grâce au nom du champ. Cependant, dans le cas où nous avons des champs imbriqués, le nom du champ en question n'est plus suffisant. En effet, pour récupérer cet élément, Drupal a besoin du nom des champs parents. Nous avons donc deux solutions :
Pour permettre la soumission du formulaire, il faut 2 choses :
1. Un ou plusieurs boutons submit
Voici l'exemple d'un bouton submit :
$form['submit'] = [
'#type' => 'submit',
'#value' => 'Valider',
'#weight' => 100
];
2. Et renseigner la méthode submitForm, qui sera automatiquement appelée si toutes vos validations ont été passées avec succès. C'est au sein de cette méthode que vous pouvez récupérer les données renseignées dans le form_state, les utiliser et/ou les sauvegarder dans votre structure de données. Vous pouvez également rediriger l'utilisateur en utilisant la méthode $form_state→setRedirectUrl.
public function submitForm(array &$form, FormStateInterface $form_state) {
$this->manager->setInformation($form_state->getValues());
$form_state->setRedirect('route_name');
}
Toujours dans une optique de découper proprement mon code et de le maintenir dans le temps, vous pouvez remarquer qu'ici je délègue la responsabilité du traitement des données du formstate à une autre classe. Classe que l'on pourrait désigner par Manager.
> Et si nous avons plusieurs boutons, comment fait-on ? Que ce soit de manière synchrone ou asynchrone, il n'y a pas de champs de type button qui puisse imiter le comportement du champ submit. En revanche il est possible d'ajouter autant d'éléments submit que l'on souhaite.
Ce ne sera plus la méthode submitForm qui sera appelée, mais la méthode qui sera renseigné dans le paramètre #submit du render array correspondant.
Pour gérer l’affichage de son formulaire, deux solutions se présentent à nous :
La plus « Drupalienne » des solutions serait celle des render array. En effet, ils ont pour rôle de produire du « rendu » à partir des données.
Nous avons vu précédemment qu'il existait plusieurs options pour gérer l’affichage. Ici aussi, il est possible de rajouter tous les attributs que nous souhaitons (id, class, placeholder, etc.), gérer les poids des éléments, leurs titres (label) ainsi que leur description.
En revanche, cette approche présente quelques inconvénients :
Les deux premiers points sont malheureusement inhérents aux render array et à Drupal, et ne peuvent donc être contournés. En revanche il existe une solution pour déporter une petite partie de la logique au niveau front.
Un possibilité qu’offre les render array est d’associer au tableau de rendu : un « theme ». Ce qui correspond à un template. Pour ceci, il faut :
1. Déclarer le thème dans le .module
example.module
function example_theme() {
return [
'my_form_theme' => [
'render element' => 'form',
],
];
}
2. Renseigner le thème dans le render array
Form/MyAwesomeForm.php
class MyAwesomeForm extends FormBase {
public function buildForm(array $form, FormStateInterface $form_state) {
$form = parent::buildForm($form, $form_state);
$form['#theme'] = 'my_awesome_form';
[...]
return $form;
}
[...]
}
3. Personnaliser son template
templates/my-awesome-form.html.twig
Il est possible d’insérer des petits morceaux de formulaire au sein de son propre template, en inscrivant , key étant la clé du render array contenant le render élément à afficher.
Form API propose la possibilité d’insérer de l’AJAX dans son formulaire. Cela mériterait bien un article à part entière, alors je vais seulement vous présenter les « basics ».
Pour insérer la notion d’AJAX au sein d’un élément, il suffit de lui ajouter la propriété #ajax et d’y insérer quelques propriétés :
'#ajax' => [
'callback' => '::myAjaxCallback', // don't forget "::" when calling a class method.
//'callback' => [$this, 'myAjaxCallback'], //alternative notation
'event' => 'change',
'wrapper' => 'edit-output', // This element updated with this AJAX callback.
]
Lorsque l’évènement est déclenché, le formulaire envoie une requête AJAX qui appelle la méthode callback renseigné ci-dessus. Il reste maintenant à déterminer ce que va venir modifier ce formulaire « à la volée » et donc ce que devra renvoyer cette méthode callback afin de procéder aux changements.
Là, Drupal vous laisse le choix :
1. Soit vous renvoyez un render element comme dans l'exemple ci-dessous. Ce que je trouve le plus simple et le plus intuitif.
public function myAjaxCallback(array &$form, FormStateInterface $form_state) {
// Prepare our textfield. check if the example select field has a selected option.
if ($selectedValue = $form_state->getValue('example_select')) {
// Get the text of the selected option.
$selectedText = $form['example_select']['#options'][$selectedValue];
// Place the text of the selected option in our textfield.
$form['output']['#value'] = $selectedText;
}
// Return the prepared textfield.
return $form['output'];
}
Dans ce cas, nous récupérons l’input à modifier dans le tableau $form
, nous modifions cet élément, puis nous le renvoyons. Drupal sait ainsi qu’il doit modifier l’affichage de cet élément (sans rafraîchir la page, la magie de l’AJAX !)
2. Soit vous renvoyez une portion de HTML (html markup).
public function myAjaxCallback(array &$form, FormStateInterface $form_state) { $markup = 'nothing selected'; // Prepare our textfield. Check if the example select field has a selected option. if ($selectedValue = $form_state->getValue('example_select')) { $selectedText = $form['example_select']['#options'][$selectedValue]; $markup = "
$selectedText"; } // Don't forget to wrap your markup in a div with the #edit-output id // or the callback won't be able to find this target when it's called // more than once. $output = "
<div id='edit-output'>$markup</div>"; // Return the HTML markup we built above in a render array. return ['#markup' => $output]; }
Avez-vous remarqué que dans l'exemple ci-dessus, la portion de HTML a été encapsulée dans un wrapper (défini plus haut) comme tel :
'wrapper' => 'edit-output'
3. Enfin, vous choisissez d’envoyer des AjaxCommand. Ce sont des objets qui vont se transformer en instruction au niveau de client.
En conclusion, la Form API peut être difficile à appréhender et peut parfois même s'apparenter à une usine à gaz, d'autant que cet article n'a pas la prétention d'être exhaustif sur ce sujet !
Mais lorsque l'on a compris son fonctionnement, son utilisation peut vraiment vous faire gagner du temps ! Personnellement, je vous conseille de commencer par l'utiliser pour des formulaires simples, de jouer avec les formElements et une fois prêt, lancez-vous dans l'AJAX !
Cet article vous a été utile ? Je vous conseille de lire également les articles suivants :
Crédit photo : gorodenkoff