Drupal 6 Tabledrag Forms

A tabledrag form in Drupal 6 is any form that will allow you to move items up and down the list or into a hierarchy of items. This is actually a component of Drupal itself and is used on forms like menu, taxonomy and blocks management. Tabledrag is a great way of allowing your users to move items up and down a list with ease and they will be used to the mechanism from other areas of the site.

Essentially, a tabledrag is a normal HTML table within a form that contains some form elements and some JavaScript that will turn the table into a sortable group of elements.

I needed to create a tabledrag form in a recent project, but after not finding many good tutorials about creating a fully working form online I decided to write one. This will take you through the basics of what you need to get a weight based tabledrag form working and will also leave you with a working module.

The first thing we need to do is create a module for our tabledrag form to work from. Create a module folder called mycustomtabledrag and create the following mycustomtabledrag.info file.

name = Custom Table Drag
description = Creates a tabledrag element
version = 1.0
core = 6.x

Next, create a file called mycustomtabledrag.module and place it in the mycustomtabledrag module directory. This will only contain the below file stub at the moment, but we will add functions to it later.

<?php

/**
 * @file
 *   The mycustomtabledrag module file.
 */

In order to get a tabledrag form to work properly you need to have a list of components that contain a weight value. This weight value is an integer value that gets updated when the elements of the form are moved around and is essential for the correct working of the form. Rather than mess about with array values I thought it would be better to create real data that would be updated by the form. To this end I have created a database table that will store email and person data for use in the module.

Create a file called mycustomtabledrag.install and put it into your module directory along with your mycustomtabledrag.info file. The .install file is used by Drupal when installing and uninstalling the module. I won't go into too much detail here (especially as all this information is available on drupal.org) but what essentially happens is that Drupal will create a table called mycustomtabledrag_emaillink that will contain id, person, email and weight.

Here is the code for the mycustomtabledrag.install file. If you enable the module now you will find that not much happens (there isn't any module code yet) but the table has been created.

<?php

/**
 * @file
 *   My custom tabledrag install file.
 */

/**
 * Implementation of hook_install()
 */
function mycustomtabledrag_install() {
  drupal_install_schema('mycustomtabledrag');
}

/**
 * Implementation of hook_uninstall()
 */
function mycustomtabledrag_uninstall() {
  drupal_uninstall_schema('mycustomtabledrag');
}

/**
 * Implementation of hook_schema().
 */
function mycustomtabledrag_schema() {
  $schema['mycustomtabledrag_emaillink'] = array(
    'description' => 'People linked to email addresses.',
    'fields' => array(
      'id' => array(
        'description' => 'The person and email ID',
        'type' => 'serial',
        'not null' => TRUE,
      ),
      'person' => array(
        'description' => 'The person',
        'type' => 'varchar',
        'length' => '255',
        'not null' => TRUE,
      ),
      'email' => array(
        'description' => 'The email associated with this person',
        'type' => 'varchar',
        'length' => '255',
        'not null' => TRUE,
      ),
      'weight' => array(
        'description' => 'The weight',
        'type' => 'int',
        'not null' => FALSE,
      ),
    ),
    'primary key' => array('id'),
    'indexes' => array(
    'person' => array('person'),
    ),
  );
  return $schema;
}

Now that we have a data resource to work with (i.e. the mycustomtabledrag_emaillink table) we can now flesh out the rest of the module to create the tabledrag form.

The first step is to register a menu callback function to display a page that will show the form. This will create a page that will call the drupal_get_form() function and pass it the return value of the mycustomtabledrag_emaillink_form() function and therefore displaying the form.

/**
 * Implementation of hook_menu()
 */
function mycustomtabledrag_menu() {
  $items = array();

  // Add a callback page to display the form
  $items['mycustomtabledrag'] = array(
    'title' => 'Custom Table Drag',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('mycustomtabledrag_emaillink_form'),
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );

  return $items;
}

The next step is to register a theme function for the rendering of the tablesort element. We will come to the theme function later on, but this hook is needed by Drupal to know what theme functions are available in our module. The only attribute we are passing to the function is the form.

/**
 * Implementation of hook_theme().
 */
function mycustomtabledrag_theme() {
  return array(
    'mycustomtabledrag_emaillink_form' => array(
      'arguments' => array(
        'form' => NULL
      ),
    ),
  );
}

Now we have all of the hooks in place we can define the form creation function. This will define a form with the tablesort element (in the form of a set of row elements), a fieldset containing the fields needed to enter data into the table, and submit the form.

To create the rows we first query the mycustomtabledrag_emaillink table for all data (ordered by weight) and insert it into the form as a group of rows elements. Each item in the array then contains each data item as a form element. If we ran the module with the currently inserted code then we would see a list of form elements, but no tabledrag element. It is the theme function that converts this array of rows into a table and adds the tabledrag functionality.

One important thing to note from the function code is the addition of a class to the weight elements. This is used by the tablesort JavaScript code to hide the weight elements and update them when the order of the elements has been changed.

/**
 * Implementation of hook_form().
 */
function mycustomtabledrag_emaillink_form(&amp;$form_state) {
  $form = array();
  $form['#method'] = 'post';

  // Fetch the data for the tablesort element
  $result = db_query("SELECT id, person, email, weight
            FROM {mycustomtabledrag_emaillink}
            ORDER BY weight ASC");

  while ($row = db_fetch_object($result)) {
    $form['rows'][$row->id]['person_' . $row->id] = array(
      '#type' => 'textfield',
      '#size' => 35,
      '#default_value' => $row->person,
    );

    $form['rows'][$row->id]['email_' . $row->id] = array(
      '#type' => 'textfield',
      '#size' => 35,
      '#default_value' => $row->email,
    );

    // now create the weight form element.
    $form['rows'][$row->id]['weight_' . $row->id] = array(
      '#type' => 'weight',
      '#size' => 5,
      '#delta' => 50,
      '#default_value' => $row->weight,
      //add a specific class in here - we need this later
      '#attributes' => array('class' => 'weight'),
    );
  }

  $form['person_fieldset'] = array(
    '#type' => 'fieldset',
    '#title' => t('New Person And Email Combination'),
    '#collapsible' => TRUE,
    '#collapsed' => TRUE
  );
  $form['person_fieldset']['person_0'] = array(
    '#type' => 'textfield',
    '#title' => t('Person'),
  );
  $form['person_fieldset']['email_0'] = array(
    '#type' => 'textfield',
    '#title' => t('Email'),
  );

  //Don't forget the submit button
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save changes'),
  );

  return $form;
}

With all this in place we can now set about applying a theme to the tabledrag form. The mycustomtabledrag_theme() function above detailed a function called theme_mycustomtabledrag_emaillink_form(). The important thing to realise here is that because we have called the form definition function mycustomtabledrag_emaillink_form(), any theme function we define with the name 'theme_[form name]' will be picked up and used by Drupal to theme the form. This is important, and as you will probably change the function names when you copy the code you must take this into account.

What this function does is to loop through each of the 'rows' we defined in the form and render the form elements it contains. Once complete we then render the elements as a table before storing the result back into the rows array as a HTML form element. After the form is rendered we run a function called drupal_add_tabledrag() to let Drupal know that our form contains a tabledrag component. The drupal_add_tabledrag() function will add the needed JavaScript elements to the page so that the tabledrag element will work.

/**
 * Theme function for the mycustomtabledrag_emaillink_form form.
 */
function theme_mycustomtabledrag_emaillink_form($form) {
  $table_rows = array();

  if (is_array($form['rows'])) {
    foreach ($form['rows'] as $id => $row) {
      // We are only interested in numeric keys
      if (intval($id)) {
        $this_row = array();

        $this_row[] = drupal_render($form['rows'][$id]['person_' . $id]);
        $this_row[] = drupal_render($form['rows'][$id]['email_' . $id]);

        // Add the weight field to the row
        $this_row[] = drupal_render($form['rows'][$id]['weight_' . $id]);

        // Add the row to the array of rows
        $table_rows[] = array('data' => $this_row, 'class' => 'draggable');
      }
    }
  }

  // Set the table headers.
  $header = array(
    "Person",
    "Email",
    "Weight"
  );

  // Render the form elements into a table.
  $form['rows'] = array(
    '#value' => theme('table', $header, $table_rows, array('id' => 'person-email'))
  );
  
  // Render the entire form
  $output = drupal_render($form);

  // Call drupal_add_tabledrag to add and setup the JavaScript.
  // The first parameter is the table ID, which is crutial in finding the correct table.
  // The fourth parameter is the class of the form item which holds the weight
  drupal_add_tabledrag('person-email', 'order', 'sibling', 'weight');

  return $output;
}

The above code contains the simplest version of a tabledrag form, where elements are only moved up and down. The drupal_add_tabledrag() function takes a number of parameters, which are worth going through individually as they can change the behaviour of the form.

From the above attributes it be seen that a hierarchy can be established between different elements, but the variables we have set up in the mycustomtabledrag module only allow weight based ordering to be set up. To create a hierarchy of options you will need to create a variable that can store the relationship between different elements and also a means of storing it. If you want to create a module that has a hierarchy then you need to make that decision early on in your module design so that you can accommodate it into your code.

The final part in this module is the addition of submit and validation functions to our form so that we can ensure data validity before saving it. You can either hard code them into the form definition, or you can write them as hook_form_submit() and hook_form_validate() functions. The following code contains some simple validation for the email address and allows the saving and updating of data from the form.

/**
 * Implementation of hook_form_validate().
 */
function mycustomtabledrag_emaillink_form_validate($form, &$form_state) {
  foreach ($form_state['values'] as $key => $value) {
    if (($key == 'email_0' &amp;&amp; trim($value) != '') || ($key != 'email_0' &amp;&amp; strpos($key, 'email_') !== FALSE)) {
      if (valid_email_address($form_state['values'][$key]) == FALSE) {
        form_set_error($key, t('Please enter a valid email address'));
      }
    }
  }
}

/**
 * Implementation of hook_form_submit().
 */
function mycustomtabledrag_emaillink_form_submit($form, &$form_state) {
  $emaillink = array();

  foreach ($form_state['values'] as $key => $value) {
    if (strpos($key, 'person_') !== FALSE || strpos($key, 'email_') !== FALSE || strpos($key, 'weight_') !== FALSE) {
      $replace_elements = array('person_', 'email_', 'id_', 'weight_');
      // Extract the id and the field from the element data
      $emaillink[str_replace($replace_elements, '', $key)][substr($key, 0, strpos($key, '_'))] = $value;
    }
  }

  // Save the items we have recovered from the form values.
  foreach ($emaillink as $id => $email) {
    if ($id == 0 &amp;&amp; $email['person'] != '' &amp;&amp; $email['email'] != '') {
      db_query("INSERT INTO {mycustomtabledrag_emaillink}(person, email, weight) VALUES ('%s', '%s', 0)", $email['person'], $email['email']);
    } elseif ($email['person'] != '' &amp;&amp; $email['email'] != '') {
      db_query("UPDATE {mycustomtabledrag_emaillink} SET person = '%s', email = '%s', weight = %d WHERE id = %d LIMIT 1", $email['person'], $email['email'], $email['weight'], $id);
    }
  }
  drupal_set_message('The email form has been updated.');
}

What about deleting items from the table? This can be done in a number of ways, but the best way is probably to provide a link on the table that will delete the item in question. As this isn't really a component of the tabledrag functionality I have left it out as a task for the reader.

Here are the parameters for the drupal_add_tabledrag() method in full.

  • $table_id - This is a string containing the ID of the table we want to use as a tabledrag table. In the case of our project this table ID is 'person-email', which was set when we rendered the rows into a table.
  • $action - This is a string that describes the action to be done for the form item and can be either 'match', 'depth', or 'order'. Order (which is used in this module) is used to set weights on other form elements with the same group. Match is used for parent relationships and depth updates the target element with the current indentation.
  • $relationship - A string describing where the $action variable should be performed, which will be either 'parent', 'sibling', 'group', or 'self'.
    • Parent will affect elements up the tree.
    • Sibling will look for fields in the same group as the rows above and below it. This is used in the module code above as we only need to change the weight of a couple of elements.
    • Self affects the dragged row itself.
    • Group affects the dragged row and any children below it. This means that if a group is dragged then everything below it will also be carried with it.
  • $group - This is the class name used to group all related items together. In the example module we have created here the class we have used to group things together is 'weight', which contains a form element that is updated when the table is changed.
  • $subgroup - (optional) This is a string that defines a field that is to be used in creating hierarchy. This is used in much the same way as the $group parameter, but is only used when setting up a table with hierarchy. The usage of this parameter can be changed by the value of the $action parameter.
  • $source - (optional) If the $action is 'match', this string should contain the class name identifying what field will be used as the source value when matching the value in $subgroup.
  • $hidden - (optional) This is a string that can be used to hide a column from view. The default behaviour here is to hide the weight and parent form elements, but it can also be set to FALSE to stop anything being hidden.
  • $limit - (optional) This is an integer denoting the maximum number of levels that a hierarchy can have. For the purposes of this module we haven't set this, so the value will be 0.

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
8 + 7 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.