Les formulaires dynamiques sont vieux comme le client-serveur. Cela peut correspondre par exemple à une liste principale dont le choix d'un élément déclenche la population d'une liste secondaire. Rien de bien sorcier donc, mais comme pour pas mal d'autres choses, ce qui était relativement simple à coder avec un
RAD
comme Delphi, ou même Visual Basic, est devenu un véritable enfer avec la mode des applications WEB. Voyons donc comment faire ce type de chose avec la dernière Form API de Drupal 6.
Les sources du module d'exemple courses sont disponibles ici.
Comme depuis un moment déjà nous allons continuer à torpiller notre liste de courses à qui nous avions récemment ajouté les nouveaux schémas de Drupal 6.
Pour les besoins de l'expérience, nous allons ajouter deux nouveaux champs à notre table node_produit : categorie_produit et type_produit. Le but est simple, lors de l'ajout ou de l'édition d'un produit, nous aurons une liste permettant de choisir la catégorie (féculents, jus de fruits, légumes, etc.). Et lorsque l'utilisateur sélectionnera un élément de cette liste, cela déclenchera la mise à jour d'une seconde liste de types de produit (pâtes, choux, jus d'orange, etc.). Un peu neu-neu, je sais, mais au moins on peut se concentrer sur la technique.
Pour commencer nous allons rapidement modifier le schéma de notre module (courses.install) en ajoutant nos deux champs :
courses.install - courses_schema()
Ensuite, il nous faut implémenter un nouveau hook_update pour permettre la mise à jour des anciens schémas :
courses.install - courses_update_2()
Ceci fait, lancez la procédure de mise à jour de Drupal (update.php) au terme de laquelle, note table devrait être modifiée.
Pour une véritable application, nos données catégories et types seraient proprement stockées en base de donnée. Ici, nous allons faire simple avec deux fonctions en dur :
courses.module
A l'ancienne mode, cela aurait consisté à utiliser le très vilain attribut de formulaire DANGEROUS_SKIP_CHECK et ajouter un bouton qui provoque une validation intermédiaire. Aujourd'hui trois arguments s'y opposent. Tout d'abord DANGEROUS_SKIP_CHECK a été supprimé. Ensuite les formulaires sont tous mis en cache et donc difficile à modifier dynamiquement. C'est ceci dit faisable en utilisant l'attribut de champ #process et de formulaire #REBUILD mais le fait de ne pas pouvoir by-passer les contrôles implique qu'à chaque mise à jour de la liste s'affiche des erreurs de validation, c'est moche. Enfin dernier argument, c'est pas AJAX donc c'est pas bien, on m'a dit...
Le framework AHAH qui était un module pour Drupal 5, fait aujourd'hui parti du coeur de Drupal 6. Cette librairie utilise jQuery pour ajouter à Drupal cette giclée d'AJAX qui lui manquait temps. La différence entre AJAX et AHAH (Asynchronous HTML over HTTP, me demandez pas pourquoi) est que le résultat de la réponse est du XHTML qui est directement collée dans le document en cours avec de petits effets genre glissement, fondus, etc...
Il faut donc voir AHAH comme un sous-ensemble fonctionnel d'AJAX et cela va nous suffire car c'est exactement ce dont nous avons besoin.
L'intégration dans un formulaire d'AHAH est relativement directe. Pour nous deux listes, cela donne ceci (à placer à la tête de la fonction courses_form :
courses.module - courses_form()
Simple mais nécessitant un peu d'explication. Le début de l'élément de formulaire categorie_produit ne change pas par rapport à ce que nous connaissions. Elle est alimentée par la fonction courses_categories() que nous avons défini plus haut et utilise $node->categorie_produit comme valeur par défaut.
Là où cela change, c'est justement avec l'attribut #ahah qui définit un comportement AJAX, pardon AHAH, que la liste doit adopter. Le bloc AHAH n'ayant pas d'attribut event, va aller se connecter à l'événement par défaut, à savoir la sélection d'un élément de la liste. Nous aurions pu rajouter un 'event'=>'mousedown' mais cela n'aurait pas grand intérêt. wrapper indique l'ID d'un DIV qui va recevoir les données, method dit que cette réception doit donner lieu à un remplacement de ce que contenait le DIV (cela pourrait être before ou after), effect, c'est pour faire joli, mettez none si vous n'aimez pas, et enfin path est l'URL vers laquelle le module AHAH doit émettre une requête pour recevoir ce fameux contenu à coller dans le DIV.
Ce fameux DIV est défini par l'élément de formulaire suivant avec comme ID, celui qui a été donné plus haut, et comme contenu notre fameux champ dynamique. Et comme il est dynamique, sa construction est placée dans une fonction que nous allons maintenant définir :
courses.module - courses_types_field()
Rien de compliqué là dedans, il s'agit juste de la récupération de la bonne liste de types en fonction de la catégorie et éventuellement de la définition d'une position par défaut si le paramètre $type est renseigné (cas de l'édition d'un produit).
Voilà, le décor est en place, passons à la partie rock'n'roll, la réponse à la requête AHAH.
Comme nous l'avons vu, le module AHAH est censé lorsque l'utilisateur sélectionne un élément de la liste categories, émettre une requête vers courses/js/types de sorte à recevoir le nouvel élément de formulaire qui va aller remplacer l'ancien. Pour que Drupal sache répondre à cette requête, il faut donc déjà rajouter un nouveau menu (à placer avant le return $items :
courses.module - courses_menu()
Rien de nouveau ici, cela reprend la technique plus laborieuse que j'avais utilisée pour faire causer jQuery avec Drupal. Il nous reste donc à ajouter notre callback :
Alors oui, j'en conviens, c'est un peu "sportif". L'idée est que AHAH ne fait pas un GET mais un POST du formulaire dans son état courant. Du coup, nous avons toutes les valeurs que l'utilisateur a déjà saisies, dont la catégorie, dans la variable $_POST. Cela nous permet déjà de construire notre élément dynamique.
Une valeur un peu étonnante envoyée par POST est form_build_id. Il s'agit de l'ID unique de l'instance du formulaire pour cet utilisateur. Et nous allons utiliser cet ID pour aller faucher dans le cache le formulaire complet tel que Drupal l'a sauvegardé avant de l'envoyer. Ensuite nous allons remplacer dans ce formulaire l'ancien élément type_produit par le nouveau et sauver le tout dans le cache. Alors pourquoi se compliquer la vie ainsi ? Simplement pour tromper Drupal et lui faire croire que le formulaire que nous sommes en train de modifier dans son dos est le même que celui qu'il a originellement envoyé à l'utilisateur.
Pour terminer, nous allons utiliser la fonction form_builder qui va régénérer le formulaire dans le même état que si Drupal était sur le point de l'envoyer. La seule différence est que nous allons extraire notre élément 'type_produit' de ce formulaire regénéré pour le passer à la fonction drupal_render qui va le transformer en code XHTML.
Dernière étape, l'utilisation de drupal_to_js qui va transformer ce code XHTML en un fragment au format
JSON
que le module AHAH du client est capable de comprendre. Une fois cette dernière transformation faite, le tout est simplement envoyé au client.
Notez la fonction exit() qui arrête le traitement ici, interdisant à Drupal tout autre opération.
Voilà, c'est tout et ça marche très bien. Ne vous laissez pas trop effrayer par l'apparente complexité de l'approche car je l'ai développée au maximum. Il est possible de créer une ou deux fonctions génériques qui permettraient de faire la même chose en quelques lignes. Si cela continue dans cette voie, on va finir par pouvoir faire des choses aussi basiques que celles-ci avec la même simplicité que les Delphi & co d'il y a 10 ans... Enfin, je rêve un peu, avec les client Riches, il a fort à parier que tout cet acquis soit à nouveau remis en jeu...