Ajaxifier les formulaires de noeuds

Onglets principaux

La documentation Drupal 6 n'est plus maintenue et en cours de dépublication.

Traduction d'un article en anglais se trouvant ici : http://www.stellapower.net/content/ajax-ifying-drupal-node-forms

Recemment, pour la première fois avec Drupal6, j'avais besoin de créer un formulaire où un nombre variable de champs pourraient y être ajoutés en cliquand simplement sur un bouton "ajouter".
Je voulais construire un noeud où les utilisateurs pourraient créer une compilation personnalisée d'albums de leurs meilleurs morceaux. Cependant, le nombre de morceaux pourrait varier d'un album à l'autre donc je voulais une solution qui permette à l'utilisateur d'ajouter des morceaux sans avoir à recharger la page.
Maintenant, oui, j'aurais pu utiliser CCK pour construire un contenu personnalisé, mais je voulais voir comment cela pourrait etre fait en utilisant seulement l'api de formulaires de Drupal.
J'ai utilisé le module "sondages" comme exemple pour l'intégrer.

Le processus est plûtot simple si vous êtes familier(e) avec l'api de formulaires de Drupal.
Cependant, beaucoup de code est necessaire mais c'est plus pour l'autre partie du formulaire de noeud que l'AJAX en lui même.
En fait, le code nécessaire pour la fonctionnalité AJAX est plutôt court, et l'est encore plus court dans Drupal 7.

La première étape pour créer un nouveau type de contenu est d'implémenter hook_node_info(). Ceci informe Drupal sur le type de contenu personnalisé défini par notre module, que nous appellerons "album".

<?php
function album_node_info() {
  return array(
   
'album' => array(
     
'name' => t('Compilation album'),
     
'module' => 'album',
     
'description' => t('Create your very own custom compilation album'), // Créez votre compilation d'albums personnelle.
     
'title_label' => t('Album name'), // nom de l'album
     
'body_label' => t('Description'),
    ),
  );
}
?>

Une fois que nous avons fait cela, nous avons besoin de définir le formulaire du noeud en lui même. Au minimum, j'ai décidé qu'il devrait avoir un titre, une brêve description suivie de la liste des morceaux.
Contrairement au module "sondages", je ne voulais pas que des morceaux soient déjà ajoutés. A la place, je voulais afficher leurs détails dans une table, suivis d'un lien de suppression pour chacun qui autoriserait l'utilisateur à détruire les morceaux individuellement.
Les nouveaux morceaux pourraient être ajoutés à la listeen utilisant un ensemble de champs de formulaires affichés sous la table avec un bouton "Ajouter un nouveau morceau".

Donc commençons par définir nos champs de formulaire pour le type de contenu.

<?php
function album_form(&$node, $form_state) {
 
$type = node_get_types('type', $node);

 
// Titre.
 
$form['title'] = array(
   
'#type' => 'textfield',
   
'#title' => check_plain($type->title_label),
   
'#default_value' => $node->title,
   
'#required' => TRUE,
   
'#weight' => 0,
  );

 
// Champ corps de texte.
 
$form['body_field'] = node_body_field($node, $type->body_label, $type->min_word_count);

 
// Definir un container dans lequel placer
  // la liste des morceaux et le formulaire "ajouter un morceau".
 
$form['track_wrapper'] = array(
   
'#tree' => FALSE,
   
'#weight' => 5,
   
'#prefix' => '<div class="clear-block" id="album-track-wrapper">',
   
'#suffix' => '</div>',
  );

 
// Définir un ensemble de champs qui contiendra les champs de formulaire
  // pour le titre du morceau et le nom de l'artiste, suivis du bouton "ajouter morceau".
 
$form['track_wrapper']['add_track'] = array(
   
'#type' => 'fieldset',
   
'#title' => t('Add another track'), // Ajouter un autre morceau
   
'#tree' => FALSE,
   
'#weight' => 6,
  );

 
// Définir les éléments de formulaire pour le titre du nouveau morceau et le nom de l'artiste
 
$form['track_wrapper']['add_track']['new_track'] = array(
   
'#tree' => TRUE,
   
'#theme' => 'album_add_track_form',
  );
 
$form['track_wrapper']['add_track']['new_track']['new_track_title'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Track title'), // Titre du morceau
   
'#weight' => 0,
  );
 
$form['track_wrapper']['add_track']['new_track']['new_artist'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Artist name'), // Nom de l'artiste
   
'#weight' => 1,
  );

 
// Nommons notre bouton 'album_track_more' pour éviter des conflits avec l'utilisation d'autres modules
  // utilisant des boutons AHAH avec l'id "more".
 
$form['track_wrapper']['add_track']['album_track_more'] = array(
   
'#type' => 'submit',
   
'#value' => t('Add track'), // Ajouter un morceau
   
'#weight' => 1,
   
'#submit' => array('album_track_add_more_submit'),
   
'#ahah' => array(
     
'path' => 'album_track/js/0',
     
'wrapper' => 'album-tracks',
     
'method' => 'replace',
     
'effect' => 'fade',
    ),
  );

  return
$form;
}
?>

La partie la plus importante du code ci dessus est le bouton 'Ajouter un morceau'.
Contrairement aux autres boutons de soumission que vous avez déjà vus, il possède un élément #ahah.
C'est un tableau et il définir un nombre de réglages requis pour qu'AJAX fonctionne.
L'élément le plus important est le chemin. C'est l'url qui va être questionnée au serveur quand le bouton est clické et qui retournera les données JSON nécéssaires dont nous avons besoin pour ajouter à la page courante.

Nous identifions où seront insérées les données que nous recevrons.
Dans cet exemple ci-dessus nous avons identifié le div avec l'id "album-tracks" comme emplacement.
En définissant la méthode pour remplacer les données reçues, elles ne seront pas ajoutées au div et par contre remplaceront tout son contenu existant.
Cela peut ne pas être le comportement désiré dans tous les cas. Finalement, nous avons configuré l'effet javascript de remplacement en fondu (fade). Une alternative à celà est 'slide'(glissement). Plus d'information sur les effets de remplacement peuvent être trouvées sur le site de jquery.
Nous avons aussi défini un gestionnaire de soumission pour le bouton 'Ajouter un morceau' qui supportera la configuration de notre nouveau morceau.

Cependant, il y a encore plus de travail qui doit être fait. Nous avons besoin de définir le gestionnaire de soumission album_track_add_more_submit() et la fonction pour supporter le chemin album_track/js/0.

<?php
/**
  * gestionnaire de soumission pour le bouton 'Ajouter un morceau' sur le formulaire de noeud
  */
function album_track_add_more_submit($form, &$form_state) {
 
$form_state['remove_delta'] = 0;

 
// Demande la reconstruction du formulaire et lance les supports de soumission.
 
node_form_submit_build_node($form, $form_state);

 
// Effectuer les changements que nous souhaitons à l'état actuel du formulaire.
 
if ($form_state['values']['album_track_more']) {
   
$new_track = array();
   
$new_track['track_title'] = $form_state['values']['new_track']['new_track_title'];
   
$new_track['artist'] = $form_state['values']['new_track']['new_artist'];
   
$form_state['new_track'] = $new_track;
  }
}
?>

La partie la plus importate de ce gestionnaire de soumission est l'appel à node_form_submit_build_node() qui demande la reconstruction du formulaire et lance les supports de soumission.
Elle crée aussi une nouvelle variable $form_state pour contenir les données du nouveau morceau en poussant les données soumises par l'utilisateur.
Ces données seront retournées au formulaire quand il sera reconstruit et peuvent alors être affichées sur le formulaire. La ligne remove_delta sera nécessaire plus tard quand nous ajouterons les liens pour retirer les morceaux existants du noeud.

Ensuite, nous avons besoin de définir une fonction de callback pour le chemin album_track/js/0, que nous pouvons faire dans hook_menu()

<?php
function album_menu() {
 
$items = array();

 
$items['album_track/js/%'] = array(
   
'page callback' => 'album_track_js',
   
'page arguments' => array(2),
   
'access arguments' => array('access content'),
   
'type ' => MENU_CALLBACK,
  );
  return
$items;
}
?>

Notre configuration de chemin ci dessus montre qu'une fonction de callback album_track_js doit être définie et prend l'argument de chemin final comme son seul et unique argument.
Dans notre exemple, cet argument est défini sur 0 pour le bouton 'Ajouter un morceau'. Comme nous le verrons plus tard, il sera défini sur d'autres valeurs quand nous réutiliserons la même fonction pour supporter la suppression de morceaux dans les noeuds.

<?php
function album_track_js($delta = 0) {
 
$form = album_ajax_form_handler($delta);

 
// Calculer la nouvelle sortie.
 
$track_form = $form['track_wrapper']['tracks'];
 
// Prevenir d'une duplication de conteneur.
 
unset($track_form['#prefix'], $track_form['#suffix']);

 
$output = theme('status_messages') . drupal_render($track_form);

 
// Callback de rendu final.
 
drupal_json(array('status' => TRUE, 'data' => $output));
}
?>

Cette fonction de callback supporte toutes les fonctionnalités de AJAX. Pour tous les formulaires avec fonctionnalité AJAX / AHAH il y a un jeu commun de fonctions et commandes qui doivent être lancées à chaque fois. Je les ai séparées dans leur propre fonction album_ajax_form_handler() comme cela ils peuvent être réutilisés par d'autres formulaires AJAX-ifiés.
Dans Drupal 7, une nouvelle fonction utilitaire a été ajoutée pour fournir la même fonctionnalité. En plus de cela, vous n'avez plus besoin de l'appeller du tout étant donné que c'est supporté par Drupal pour vous!

Une fois que le nouveau formulaire est reconstruit, tout ce que nos fonctions ont besoin de faire, c'est d'extraire la partie du formulaire qui nous intéresse dans $form['track_wrapper']['tracks'] et la retourner adns son format JSON.

La fonction album_ajax_form_handler() prend soin d'aller chercher le formulaire généré depuis le cache, le calculer et le reconstruire comme défini ci-dessous.

<?php
/**
* AJAX form handler.
*/
function album_ajax_form_handler($delta = 0) {
 
// Le formulaire est généré dans un fichier inclus que nous avons besoin d'inclure manuellement.
 
module_load_include('inc', 'node', 'node.pages');
 
$form_state = array('storage' => NULL, 'submitted' => FALSE);
 
$form_build_id = $_POST['form_build_id'];

 
// Obtenir le formulaire depuis le cache.
 
$form = form_get_cache($form_build_id, $form_state);
 
$args = $form['#parameters'];
 
$form_id = array_shift($args);

 
// Nous devons calculer le formulaire, le preparer pour notre cuisine en interne.
 
$form_state['post'] = $form['#post'] = $_POST;
 
$form['#programmed'] = $form['#redirect'] = FALSE;

 
// Définir notre variable d'état actuel de formulaire, nécéssaire pour supprimer les morceaux.
 
$form_state['remove_delta'] = $delta;

 
// Construire, valider et si possible soumettre le formulaire.
 
drupal_process_form($form_id, $form, $form_state);

 
// Si la validation échoue, forcer sa soumission - ceci est mon propre "hack" pour les problèmes arrivant
  // où tous les champs requis ont besoin d'être remplis avant que le bouton "Ajouter un morceau" puisse être cliqué
  // Une meilleure solution est travaillée dans la liste des travaux à réaliser pour Drupal.
 
if (form_get_errors()) {
   
form_execute_handlers('submit', $form, $form_state);
  }

 
// Cet appel reconstruit le formulaire en relayant sollennelement la variable form_state que drupal_process_form a défini.
 
$form = drupal_rebuild_form($form_id, $form_state, $args, $form_build_id);

  return
$form;
}
?>

Nous avons maintenant un formulaire qui autorise les utilisateurs à ajouter de nouveaux morceaux au noeud en utilisant AJAX / AHAH, avons défini un gestionnaire de soumission et une fonction de callback AJAX pour supporter le bouton 'Ajouter un morceau'. Cependant, nous avons omis une étape importante qui est le rendu de ces nouvelles pistes sur le formulaire quand il est reconstruit, autant que le lien 'Retirer' pour chacun. Pour faire cela, nous devons modifier notre fonction album_form() que nous avons défini précédemment.

<?php
function album_form(&$node, $form_state) {
 
$type = node_get_types('type', $node);

 
// Titre.
 
$form['title'] = array(
   
'#type' => 'textfield',
   
'#title' => check_plain($type->title_label),
   
'#default_value' => $node->title,
   
'#required' => TRUE,
   
'#weight' => 0,
  );

 
// Champ corps de texte.
 
$form['body_field'] = node_body_field($node, $type->body_label, $type->min_word_count);

 
$form['track_wrapper'] = array(
   
'#tree' => FALSE,
   
'#weight' => 5,
   
'#prefix' => '<div class="clear-block" id="album-track-wrapper">',
   
'#suffix' => '</div>',
  );

 
// Obtenir le nombre de morceaux.
 
$track_count = empty($node->tracks) ? 0 : count($node->tracks);

 
// Si un nouveau morceau a été ajouté, ajouter une liste et mettre à jour le compte des morceaux.
 
if (isset($form_state['new_track'])) {
    if (!isset(
$node->tracks)) {
     
$node->tracks = array();
    }
   
$node->tracks = array_merge($node->tracks, array($form_state['new_track']));
   
$track_count++;
  }

 
// Si un morceau est retiré, le retirer de la liste et mettre à jour le compte des morceaux.
 
$remove_delta = -1;
  if (!empty(
$form_state['remove_delta'])) {
   
$remove_delta = $form_state['remove_delta'] - 1;
    unset(
$node->tracks[$remove_delta]);
   
// Re-number the values.
   
$node->tracks = array_values($node->tracks);
   
$track_count--;
  }

 
// Container pour afficher les morceaux existants.
 
$form['track_wrapper']['tracks'] = array(
   
'#prefix' => '<div id="album-tracks">',
   
'#suffix' => '</div>',
   
'#theme' => 'album_track_table',
  );

 
// Ajouter les morceaux existants au formulaire.
 
for ($delta = 0; $delta < $track_count; $delta++) {
   
$title = isset($node->tracks[$delta]['track_title']) ? $node->tracks[$delta]['track_title'] : '';
   
$artist = isset($node->tracks[$delta]['artist']) ? $node->tracks[$delta]['artist'] : '';
   
// Affiche les morceaux existants en utilisant la fonction helper album_track_display_form().
   
$form['track_wrapper']['tracks'][$delta] = album_track_display_form($delta, $title, $artist);
  }


 
// Ajouter de nouveaux morceaux
 
$form['track_wrapper']['add_track'] = array(
   
'#type' => 'fieldset',
   
'#title' => t('Add another track'), // Ajouter un autre morceau
   
'#tree' => FALSE,
   
'#weight' => 6,
  );

 
// Définir les champs de formulaire pour le titre du nouveau morceau et le nom de l'artiste.
 
$form['track_wrapper']['add_track']['new_track'] = array(
   
'#tree' => TRUE,
   
'#theme' => 'album_add_track_form',
  );
 
$form['track_wrapper']['add_track']['new_track']['new_track_title'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Track title'), // Titre du morceau
   
'#weight' => 0,
  );
 
$form['track_wrapper']['add_track']['new_track']['new_artist'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Artist name'), // nom de l'artiste
   
'#weight' => 1,
  );

 
// Nous nommons notre bouton 'album_track_more' pour éviter les conflits avec les autres modules utilisant des boutons AHAH avec l'id "more".
 
$form['track_wrapper']['add_track']['album_track_more'] = array(
   
'#type' => 'submit',
   
'#value' => t('Add track'), // Ajouter un morceau
   
'#weight' => 1,
   
'#submit' => array('album_track_add_more_submit'),
   
'#ahah' => array(
     
'path' => 'album_track/js/0',
     
'wrapper' => 'album-tracks',
     
'method' => 'replace',
     
'effect' => 'fade',
    ),
  );

  return
$form;
}
?>

Les changements au dessus montrent que track_count est incrémentée et décrémentée si un morceau est ajouté ou retiré par la soumission AJAX. Si un nouveau morceau est ajouté, alors la variable $form_state['new_track'] définie dans album_track_add_more_submit() est mélangée avec le tableau existant de morceaux dans $node->tracks. Si un morceau est enlevé, alors le morceau spécifié est supprimé de $node->tracks. Ces morceaux sont alors calculés dans une table utilisant deux fonctions helper theme_album_track_table() et album_track_display_form().

La construction du theme à utiliser est spécifiée dans le champ $form['track_wrapper']['add_track']['new_track'] ci dessus. Dans notre cas nous avons spécifié album_track_table donc nous avons besoin de définir une fonction theme_album_track_table() avec un hook_theme(, qui est nécéssaire pour enregistrer les fonctions de theme avec Drupal.

<?php
function album_theme() {
  return array(
   
'album_track_table' => array(
     
'arguments' => array('form'),
    ),
  );
}

function
theme_album_track_table($form) {
 
$rows = array();
 
$headers = array(
   
t('Title'), // Titre
   
t('Artist'), // Artiste
   
''// Titre vide pour le lien "retirer"
 
);

  foreach (
element_children($form) as $key) {
   
// Pas besoin d'afficher le champ titre à chaque fois.
   
unset(
     
$form[$key]['track_title_text']['#title'],
     
$form[$key]['artist_text']['#title'],
     
$form[$key]['remove_track']['#title']
    );

   
// Construire la ligne de table.
   
$row = array(
     
'data' => array(
        array(
'data' => drupal_render($form[$key]['track_title']) . drupal_render($form[$key]['track_title_text']), 'class' => 'track-title'),
        array(
'data' => drupal_render($form[$key]['artist']) . drupal_render($form[$key]['artist_text']), 'class' => 'artist'),
        array(
'data' => drupal_render($form[$key]['remove_track']), 'class' => 'remove-track'),
      ),
    );

   
// Ajouter des attributs additionnels à la ligne, comme la classe pour cette ligne.
   
if (isset($form[$key]['#attributes'])) {
     
$row = array_merge($row, $form[$key]['#attributes']);
    }
   
$rows[] = $row;
  }

 
$output = theme('table', $headers, $rows);
 
$output .= drupal_render($form);
  return
$output;
}
?>

Comme nous voulons afficher les morceaux existants dans une table en tant que texte et ne pas afficher des champs éditables, nous avons besoin de définir deux éléments de formulaire pour chaque champ. Un sera un champ caché comme cela le champ de données sera accessible au gestionnaire de soumission quand le noeud sera sauvé, et l'autre est un élément de champ pour montrer les données du morceau à l'utilisateur.

<?php
/**
* Fonction helper pour définir les éléments contenus dans le formulaire pour les morceaux de l'album.
*/
function album_track_display_form($delta, $title, $artist) {

 
$form = array(
   
'#tree' => TRUE,
  );

 
// Titre du morceau.
 
$form['track_title'] = array(
   
'#type' => 'hidden',
   
'#value' => $title,
   
'#parents' => array('tracks', $delta, 'track_title'),
  );
 
$form['track_title_text'] = array(
   
'#type' => 'item',
   
'#title' => t('Title'), // Titre
   
'#weight' => 1,
   
'#parents' => array('tracks', $delta, 'track_title'),
   
'#value' => $title,
  );

 
// Artiste.
 
$form['artist'] = array(
   
'#type' => 'hidden',
   
'#value' => $artist,
   
'#parents' => array('tracks', $delta, 'artist'),
  );
 
$form['artist_text'] = array(
   
'#type' => 'item',
   
'#title' => t('Artist'), // Artiste
   
'#weight' => 2,
   
'#parents' => array('tracks', $delta, 'artist'),
   
'#value' => $artist,
  );

 
// Bouton retirer.
 
$form['remove_track'] = array(
   
'#type' => 'submit',
   
'#name' => 'remove_track_' . $delta,
   
'#value' => t('Remove'), // Retirer
   
'#weight' => 1,
   
'#submit' => array('album_remove_row_submit'),
   
'#parents' => array('tracks', $delta, 'remove_track'),
   
'#ahah' => array(
     
'path' => 'album_track/js/' . ($delta + 1),
     
'wrapper' => 'album-tracks',
     
'method' => 'replace',
     
'effect' => 'fade',
    ),
  );

  return
$form;
}
?>

Comme vous pouvez le voir, nous avons défini le bouton "retirer" à utiliser avec le même chemin de callback AJAX album_track/js/ que le bouton 'Ajouter morceau' mais avons défini le dernier argument comme étant non-zero. Nous l'avons assigné a l'id de morceau et c'est cet id qui identifie le morceau à être retiré de la liste quand le bouton "Retirer" est cliqué. Pour faire cela, nous avons besoin de faire deux changements supplémentaires. D'abord nous allons définir un nouveau gestionnaire de soumission pour ce bouton album_remove_row_submit().

<?php
function album_remove_row_submit($form, &$form_state) {
 
// Demande au formulaire de se reconstruire et lancer les supports de soumissions.
 
node_form_submit_build_node($form, $form_state);
}
?>

Finalement, nous avons besoin de modifier album_track_js() pour supporter la soumission du bouton "retirer". Comme les boutons "retirer" sont ajoutés dynamiquement au formulaire apres que la ait été générée, nous avons besoin de ré-attacher les comportements Javascript de Drupal à chaque fois que le formulaire est modifié.

<?php
function album_track_js($delta = 0) {
 
$form = album_ajax_form_handler($delta);

 
// Calculer la nouvelle sortie.
 
$track_form = $form['track_wrapper']['tracks'];
 
// Empecher la duplication des conteneurs.
 
unset($track_form['#prefix'], $track_form['#suffix']);

 
$output = theme('status_messages') . drupal_render($track_form);

 
// AHAH ne va pas être cool avec nous et n'est pas au courant pour le bouton "Retirer".
  // Ceci fait qu'il ne va pas y attacher le comportement AHAH après avoir modifié le formulaire.
  // Alors nous avons besoin de lui dire en premier.
 
$javascript = drupal_add_js(NULL, NULL);
  if (isset(
$javascript['setting'])) {
   
$output .= '<script type="text/javascript">jQuery.extend(Drupal.settings, '. drupal_to_js(call_user_func_array('array_merge_recursive', $javascript['setting'])) .');</script>';
  }

 
// Callback de rendu final.
 
drupal_json(array('status' => TRUE, 'data' => $output));
}
?>

Finalement, nous avons un formulaire de noeud avec une fonctionnalité AJAX qui nous autorise à ajouter de nouveaux morceaux et retirer les existants depuis un noeud sans avoir à recharger le formulaire en entier. Le code entier peut être téléchargé ici.

Je recommanderais aussi de verifier les sites suivants pour d'autres informations (en anglais):

Version de Drupal : 

Commentaires