Drupal: Drag and Drop Tables in Custom Modules

by Chadwick Wood
September 15th, 2010

Today I want to cover how to use those built-in drag and drop tables (the ones where you can sort the rows however you want) in Drupal, in the context of setting a weight field in a custom module. There was a pretty good article about this by Computer Minds, but I found some parts of their approach buggy, and not what I prefer, stylistically. So, consider this article my improvements upon their work.

Assumptions

This article is focusing on using the drag and drop tables, so I'm going to assume you know the basics of Drupal modules, and are familiar with the Form API. If not, you should do some reading on those, first.

Like the Computer Minds article, our code is going to be in the context of a custom module named "example". We'll cover how to define the form with the tables, theme it, and process it.

Build the Form

First off, we'll define a function that builds the form with our drag and drop table in it. This function would be called by drupal_get_form(), probably through a hook_menu() definition in your module, but again, I won't go into those specifics here. Here's the function, creating the table, and a submit button:

function example_form($form_state) {

    // this designates that the elements in my_items are a collection
    // doing this avoids the "id hack" in the Computer Minds article
    $form['my_items']['#tree'] = TRUE;

    // fetch the data from the DB
    // we're just pulling 3 fields for each record, for example purposes
    $result = db_query("SELECT id, name, weight FROM {foo} ORDER BY weight ASC");

    // iterate through each result from the DB
    while ($item = db_fetch_object($result)) {
        // my_item will be an array, keyed by DB id, with each element being an array that holds the table row data
        $form['my_items'][$item->id] = array(
            // "name" will go in our form as regular text (it could be a textfield if you wanted)
            'name' => array(
                '#type' => 'markup',
                '#value' => $item->name,
                ),
            // the "weight" field will be manipulated by the drag and drop table
            'weight' => array(
                '#type' => 'weight',
                '#delta' => 10,
                '#default_value' => $item->weight,
                '#attributes' => array('class' => 'weight'),
                ),
            );
    }

    $form['submit'] = array(
        '#type' => 'submit',
        '#value' => t('Save Changes'),
        );

    return $form;
}

Now we have the data structure with which to create our form. However, since we want to use these Javascript-based drag and drop tables, we'll need to use drupal_add_tabledrag(), which needs to be called from a theme function. So, we need to theme our form. To do that, we'll define a theme hook, and implement the form's theming function.

Theming the Form

The first thing to do is to declare a theme hook for our form. Doing this will mean that Drupal automatically calls our theming function when the form is to be displayed. To do this, implement hook_theme(), defining a hook with the same name as our form generation function:

function example_theme() {
    return array(
        'example_form' => array(
            'arguments' => array('form' => NULL),
        ),
    );
}

Now, we define the actual theme function for our form. This is where the real magic happens. We'll transform our $form data structure into a table, and render the form:

function theme_example_form($form) {
    // the variable that will hold our form HTML output
    $output = '';

    //loop through each "row" in the table array
    foreach($form['my_items'] as $id => $row) {

        // if $id is not a number skip this row in the data structure
        if (!intval($id))
            continue;

        // this array will hold the table cells for a row
        $this_row = array();

        // first, add the "name" markup
        $this_row[] = drupal_render($row['name']);

        // Add the weight field to the row
        // the Javascript to make our table drag and drop will end up hiding this cell
        $this_row[] = drupal_render($row['weight']);

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

    // Make sure the header count matches the column count
    $header = array(
        'Name',
        'Weight',
        );

    $table_id = 'my_items';

    // this function is what brings in the javascript to make our table drag-and-droppable
    drupal_add_tabledrag($table_id, 'order', 'sibling', 'weight');

    // over-write the 'my_items' form element with the markup generated from our table
    $form['my_items'] = array(
        '#type' => 'markup',
        '#value' => theme('table', $header, $table_rows, array('id' => $table_id)),
        '#weight' => '1',
        );

    // render the form
    // note, this approach to theming the form allows you to add other elements in the method
    // that generates the form, and they will automatically be rendered correctly
    $output = drupal_render($form);

    return $output;
}

That doozie should get us a form with a nice drag and drop table with just 1 column, titled "Name". The rows in the table should be draggable. The "Weight" column won't appear, since the Javascript hides it and automatically updates the weight values in the form as you drag table rows around.

Process the Form

The last thing to do is process the form. There's actually nothing special here. The Computer Minds article had to jump through a couple of hoops because of the way they implemented the form, but our (in my opinion) slicker appraoch avoids that. So, it's just a typical form processing function:

function example_form_submit($form, &$form_state) {
    // just iterate through the submitted values
    // because we keyed the array with DB ids, it's pretty simple
    foreach ($form_state['values']['my_items'] as $id => $item) {
        db_query("UPDATE {foo} SET weight=%d WHERE id=%d",$item['weight'], $id);
    }
}

This function should take into account the new order specified through drags and drops, and update the weight fields in the database appropriately.

Hope It Helped

This covers all the basics of using drag and drop tables in Drupal. I hope it's all clear; there's definitely a lot of basics to understand about forms and modules before this stuff can make sense. If you have any questions, leave them in the comments. I'm always glad to (try to) help.