Sortables in a custom node form

Draggables and sortables are commonly used in the drupal core. McAfee Clé Taxonomy, menu, cck, …  . The items that are sortable always belong to ‘a parent’. If this parent is listed as sortable in another parent, then it is a cascading system with a maximum of levels. The typical tree listing together with draggable handle icons, will tell the users that they can drag. This belong to relation controls a parent with its children that all have a positional sortorder variable. In Drupal, these are called weights. I wondered if this could be quickly implemented in custom modules where you have this relation ship. Since this is everywhere in drupal, why not do the test now? I will try to describe how to build sortable nodes in table view in a custom content type

Common examples in drupal that have this behaviour are taxonomy terms belonging to a vocabulary, menu items belonging to another menu item or menu. Examples of single level sortables are taxonomy vocabulary and blocks in regions.  Could this be altered so you can use it in your custom modules and content types? I will do this here with a custom content type slideshow that holds slides, which is a custom content type as well.  Check Install cck types, taxonomy, content and roles through multiple AJAX calls, if you want to see an example on how to import an exported cck with all its fields.

The example that goes with this is a slideshow application with slides. We hook the slideshow node form to load a list of slide nodes.  The child slides will be belong to the slideshow through a coupling table slideshow_slides with a common slideshow_id.  Slideshow_slides also holds a slide_id that relates to the slide_id of the table slides.  This way, I can clone slides to be used in other slideshows as well.  In the hook_load slideshow_load, i fetch a list of slides and Expression Studio Clé attach it to the node ($node->slides).  The hook_form will contain the custom form with a fieldset “slides” and a markup element in it with the value representing the themed output of slides.

First the code to fetch slides from the database.

  1. /**
  2.  * get list of slides belonging to a slideshow
  3.  */
  4. function slideshow_get_slides($slideshow_id, $max=35) {
  5.   static $slides;
  6.   if(!isset($slides) || count($slides) == 0) {
  7.     $result = db_query_range("SELECT n.nid, n.title, ss.*
  8.    FROM {node} n
  9.    INNER JOIN {slide} s ON n.nid = s.nid
  10.    INNER JOIN {slideshow_slides} ss ON ss.slide_id = s.slide_id
  11.    WHERE ss.slideshow_id = %d ", $slideshow_id, 0, $max);
  12.     while($row = db_fetch_object($result)) {
  13.       $slides[$row->nid] = $row;
  14.     }
  15.   }
  16.   return $slides;
  17. }

This list of slide nodes will be made sortable in a slideshow node form. The example where I looked to start off things, was the taxonomy vocabulary listing view. The only difference is, that I have to include a fieldset in the existing node form instead of building the form specific for the purpose of sorting and listing.  Most modules that uses the drupal sortables use the callback drupal_get_form where our form will go through the process of drupal saving a node.
In the hook_form function we will get the form fields array for the slides with the function slideshow_overview_slides. After that we will theme the fields to make them draggable and sortable within a slideshow. The function theme_slideshow_slides will do perform this task. Underneath are the three functions printed.

function slideshow_form

  1. function slideshow_form(&$node, $form_state){
  2.   // … the node form fields here
  3.   $form[’slides’] = array(
  4.     ‘#type’ => ‘fieldset’,
  5.     ‘#title’ => t(‘Slides in this slideshow’),
  6.     ‘#collapsible’ => TRUE,
  7.     ‘#collapsed’ => FALSE,
  8.     ‘#weight’ => -4,
  9.     ‘#tree’ => TRUE,
  10.   );
  11.   $subform = slideshow_overview_slides($node->slideshow_id, $node);
  12.   $form[’slides’][’slides_wrapper’] = theme_slideshow_slides($subform, $form_state);
  13.   // Add a button to add a slide with slideshow nid as parameter
  14.   $form[’slides’][’slides_wrapper’][‘add_slide’] = array(
  15.     ‘#type’ => ‘button’,
  16.     ‘#value’ => t(‘add slide’),
  17.     ‘#prefix’ =>,
  18.     ‘#attributes’ => array(
  19.       ‘onclick’=>‘javascript:window.location.href="/node/add/slide/’.$node->nid.‘"; return false;’
  20.     ),
  21.   );
  22.   // … the rest of the form fields
  23. }

function slideshow_overview_slides

  1. /**
  2.  * Form builder to list and manage slides.
  3.  *
  4.  * @ingroup forms
  5.  * @see slideshow_overview_slides_submit()
  6.  * @see theme_slideshow_slides()
  7.  */
  8. function slideshow_overview_slides($slideshow_id, &$node=null) {
  9.   $node->slides = slideshow_get_slides($slideshow_id);
  10.   $form = array(‘#tree’ => TRUE);
  11.   if (!empty($node->slides)) {
  12.     foreach ($node->slides as $slide_nid => $slide) {
  13.       $slide_id = $slide->slide_id;
  14.       $form[$slide_id][‘#slide’] = (array)$slide;
  15.       $form[$slide_id][‘name’] = array(‘#value’ => check_plain($slide->title));
  16.       $form[$slide_id][‘weight’] = array(
  17.         ‘#type’ => ‘weight’,
  18.         ‘#name’ => ‘weight_’.$slide_id,
  19.         ‘#id’ => ‘weight_’.$slide_id,
  20.         ‘#delta’ => 10,
  21.         ‘#default_value’ => $slide->weight,
  22.         ‘#value’ => $slide->weight
  23.       );
  24.       $form[$slide_id][‘delete’] = array(‘#value’ => l(t(‘Delete slide’), ‘node/’.$slide_nid.‘/delete’));
  25.     }
  26.   }
  27.   // sortable is needed for more than one slide
  28.   $node->num_slides = count($node->slides);
  29.   if ($node->num_slides == 1) {
  30.     unset($form[$slide_id][‘weight’]);
  31.   }
  32.   return $form;
  33. }

theme_slideshow_slides

  1. /**
  2.  * theme list of slides
  3.  */
  4. function theme_slideshow_slides($form, &$form_state) {
  5.   $rows = array();
  6.   foreach (element_children($form) as $key) {
  7.     if (isset($form[$key][‘name’])) {
  8.       $slide = &$form[$key];
  9.       $row = array();
  10.       //$row[] = drupal_render($slide['name']);
  11.       $updated = $slide[‘#slide’][‘updated’] ? ‘ <span style="color: red;"> updated</span>’ : ;
  12.       $row[] = l(t($slide[‘name’][‘#value’]), "node/".$slide[‘#slide’][‘nid’]."/edit") . $updated;
  13.       if (isset($slide[‘weight’])) {
  14.         $slide[‘weight’][‘#attributes’][‘class’] = ’slides-weight’;
  15.         $slide[‘weight’] = process_weight($slide[‘weight’]);
  16.         $weightinput = drupal_render($slide[‘weight’]);
  17.         $row[] = $weightinput;
  18.       }
  19.       $row[] = drupal_render($slide[‘delete’]);
  20.       $rows[] = array(‘data’ => $row, ‘class’ => ‘draggable’);
  21.     }
  22.   }
  23.   // Start the form
  24.   $form = array();
  25.   $form[‘#type’] = ‘markup’;
  26.   $form[‘#prefix’] =
  27. <div id="slides_wrapper">’;
  28.   $form[‘#suffix’] = ‘</div>
  29. ;
  30.   if (empty($rows)) {
  31.     $form[‘#value’] = .t(‘No slides available.’);
  32.   } else {
  33.     $header = array(t(‘Name’));
  34.     if(count($rows) > 1) {
  35.       $form[’save_sortorder’] = array(
  36.         ‘#type’ => ‘button’,
  37.         ‘#value’ => t(‘Save sortorder’),
  38.         ‘#ahah’ => array(
  39.           ‘path’ => ahah_helper_path( array(’slides’,’slides_wrapper’) ),
  40.           ‘wrapper’ => ’slides_wrapper’,
  41.           ‘event’ => ‘click’
  42.         ),
  43.       );
  44.       $header[] = t(‘Weight’);
  45.       drupal_add_tabledrag(’slides-overview’, ‘order’, ’sibling’, ’slides-weight’);
  46.     }
  47.     $header[] = array(‘data’ => t(‘Operations’));
  48.     $form[‘#value’] = theme(‘table’, $header, $rows, array(‘id’ => ’slides-overview’));
  49.   }
  50.   return $form;
  51. }

The slideshow button only is visible when there are enough items to sort, more than one to be more specific. The same thing happens with the draggable handles. Once there are enough slides and a user touches the handle of the draggable list item, a message appears to tell the user that he needs to save the sortorder. The save sortorder action is performed with an ajax call with ahah. I was testing the ahah_helper module so i used it for this as well. More details on this you find here.

The slides weight will be saved in the table slideshow_slides in this excercise. The drupal sortables work with hidden select options, which you can see thanks to firebug :) . In my form, I have this fields inside my fieldset slides and a slides_wrapper. The only thing we need to know is a value pair : the slide with its weight value. I used a quick string replace function on a conventionally named field “weight_[slide_id]” . So how i save my sortorder:

function slideshow_slides_save_sortorder

  1. /**
  2.  * Updates changed to slide weights.
  3.  *
  4.  * @see slide_form()
  5.  * @see theme_slideshow_slides()
  6.  */
  7. function slideshow_slides_save_sortorder($values) {
  8.   $sortables = array();
  9.   $parent_slideshow_id = $values[’slideshow_id’];
  10.   if(empty($values) || $parent_slideshow_id <= 0) {
  11.     return t(‘Sorry, no slides.’);
  12.   }
  13.   foreach($values as $keystring => $weight) {
  14.     $key = str_replace(‘weight_’,,$keystring);
  15.     if(is_numeric($key) &amp;&amp; is_numeric($weight)) {
  16.       $sortables[$key] = $weight;
  17.     }
  18.   }
  19.   foreach($sortables as $slide_id => $weight) {
  20.     $sql = "UPDATE slideshow_slides SET weight = %d
  21.      WHERE slideshow_id = %d AND slide_id = %d ";
  22.     db_query($sql, $weight,$parent_slideshow_id , $slide_id);
  23.   }
  24.   return t(‘Sortorder saved!’);
  25. }

Please comment on this article if you could use something from it.

This entry was posted on Tuesday, October 21st, 2008 at 8:39 pm and is filed under Drupal. You can follow any responses to this entry through the RSS 2.0 feed. Both comments and pings are currently closed.

3 Responses to “Sortables in a custom node form”

  1. neliason Says:

    Great article. I was having trouble figuring out how to get a draggable sort working. I was finally able to get it going thanks to this article.

    Now, I have another challenge to figure out. I want to be able to also have regions in my sortable data much like the blocks admin. On that page not only can you sort blocks but you can move them between regions or move them to a ‘Disabled’ region.

  2. Samos Says:

    Great stuff. I’m new to Drupal so:
    I could really use this code as a guide to create a custom node type with child rows. I can’t find something similar to this so it’s exactly what I’m looking for. Please share.

  3. msn cams Says:

    cool tips.I will try it. thanks for share good paper.