Document original à jour disponible à l'adresse http://arnumeral.fr/tutoriels/drupal/drupal-et-les-formulaires-dynamiques
Drupal et les formulaires dynamiques
mer, 24/09/2008 - 15:37 » Yoran » Tutoriels Drupal AHAH FormAPI Formulaires

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 de 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 [1].

Qu'est-ce que AHAH ?

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...

Quant utiliser AHAH ?

AJAX n'est pas obligatoirement synonyme de gain de vitesse. Chaque requête prends un temps plus ou moins long selon l'architecture technique mise en oeuvre, et ce temps a un impact sur le ressenti de l'utilisateur. La question qui se pose alors est de savoir comment arbitrer entre l'usage de AHAH et la programmation d'un formulaire dynamique en utilisant JQuery.

Je pense qu'il faut distinguer 3 cas de figures :

  1. Dynamisme simple via jQuery: Lorsque le dynamisme consiste en un simple jeu de cache-cache sur des éléments de taille raisonnable, typiquement pour faire apparaître des aspects optionnel du formulaire en fonction de choix fait sur certains éléments, il est sans doute plus sage d'embarquer tous les éléments de formulaires et de simplement jouer avec JQuery pour en masquer/montrer certaines parties. Il est en effet rare que la taille de l'élément dynamique soit suffisament important pour justifier un aller-retour serveur.
  2. Dynamisme évolué via JQuery/AJAX: Si la taille de l'élément dynamique est imposant ou si le contenu de ou des éléments doit changer (ex. une liste esclave d'une autre liste), avant de sortir l'artillerie AHAH il est sans doute possible d'envisager la simple récupération via jQuery/AJAX des seuls contenus des éléments à modifier. Cet approche fonctionne bien pour des listes maître/esclave (ex. Pays/Région).
  3. Dynamisme de structure via AHAH: En revanche, si l'élément dynamique change la structure même du formulaire, là AHAH se justifie plainement. L'exemple serait typiquement une liste d'action et un panneau de paramétrage de l'action totalement différent d'une action à l'autre.

Une fois encore, ces 3 cas de figure sont parfaitement pris en charge par AHAH mais il est je pense plus sâge de savoir choisir la bonne option correspondant au bon cas de figure.

Mise en oeuvre d'AHAH

Pour notre exemple, nous allons créer un formulaire qui va prendre

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 :

$form['categorie_produit'] = array(
'#type' => 'select',
'#title' => t('Catégorie'),
'#options' => courses_categories(),
'#default_value'=>$node->categorie_produit,
'#description' => t('Sélectionnez une catégorie'),
'#required' => TRUE,
'#ahah' => array(
'path' => 'courses/js/types',
'wrapper' => 'wrapper-types',
'method' => 'replace',
'effect' => 'fade'),  
);

$form['wrapper-types'] = array(
'#prefix' => '<div id="wrapper-types">',
'#suffix' => '</div>',
'type_produit' => courses_types_field($node->categorie_produit, $node->type_produit),
);
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 :

function courses_types_field($categorie=null,$type=null) {
if (!empty($categorie)) {
$types=courses_types($categorie);
} else {
$types=array();
}
array_unshift($types, t("Type de produit"));
return array(
'#type' => 'select',
'#title' => t('Type'),
'#options' => $types,
'#description' => t('Sélectionnez un type'),
'#default_value'=>$type,
'#required' => TRUE,
'#disabled'=>count($types)==1,
);
}
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.

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 :

$items['courses/js/types'] = array (
'page callback' => 'courses_js_types',
'type' => MENU_CALLBACK,
'access callback' => 'node_access',
'access arguments' => array ('view',1)
);
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 :

function courses_js_types() {
// Récupération de la catégorie
$categorie=$_POST['categorie_produit'];

// Fabrication de notre élément avec la bonen catégorie
$element=courses_types_field($categorie);

// Récupération de l'ID unique du formulaire
$form_build_id = $_POST['
form_build_id'];

// On fabrique un faux form_state
$form_state = array('
submitted' => FALSE);

// Récupération du formulaire à partir du cache
$form = form_get_cache($form_build_id, $form_state);

// On ajoute notre élément dynamique dans le formulaire (en fait, on remplace l'
ancien...)
$form['wrapper-types']['type_produit']=$element;

// Sauvegarde du formulaire dans le cache
form_set_cache($form_build_id, $form, $form_state);

// Reconstruction du formulaire
$form = form_builder($_POST['form_id'], $form, $form_state);

// Récupération de notre élément reconstruit
$element = $form['wrapper-types']['type_produit'];

// Transformation de l'élément en HTML
$output = drupal_render($element)

// On renvoie au client le formulaire sous sa forme HTML, convertie en JSON
print drupal_to_js(array('
data' => $output, 'status' => true));
exit();
}  

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.

Conclusion

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...