Drupal: Drag and Drop Tables in Custom Modules

Drag and Drop Tables in Drupal

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.

Comments

Anonymous's picture

The values in $form_state['values'] are flattened so you will not be able to loop over them like you have suggested in your form submit handler.

In your example each rows weight element will overwrite the returned weight variable because they are not uniquely named.

This is why the ComputerMind example was written a little differently and used 'weight-'.$id to name each of the weight elements.

Chadwick Wood's picture

Anonymous, the line:

$form['my_items']['#tree'] = TRUE;

... in example_form() in my code is what prevents the flattening you talk about, which is what allows my approach to avoid the hack in the ComputerMind example. Have you given it a try?

Anonymous's picture

So sorry! Missed that part. It makes sense now. Thanks

David's picture

Can you add this theming to form in the configuration section of a custom block? I don't know how (or if it's possible) to alter the theme on that form.

Chadwick Wood's picture

David, I'm not sure I follow your question. I would think you could use this approach in a custom block that's generating a form, but I haven't tried this.

David's picture

Hi Chadwick, I wanted the form in my 'configure' section of my block to have a list of drag and drop form items. If it was in the 'view' section I could follow your instructions, but the 'configure' op of the hook_block only takes a form element, not an html fragment.

Is that more clear?

Chadwick Wood's picture

David, I haven't done much with block configuration forms, so I can't really say. If there's a way to work a drupal_get_form() call into the configuration hook, then I think the general approach in this article could work. But that's about the best feedback I can give you on this.

Nico's picture

Thanks for the example, it was very usefull i've made for myself a ready to use simple module around it for testing: http://www.ulterius.nl/sites/default/files/download/tabledrag-6.x-1.tgz

dolphin's picture

Hi, Very nice article and very good example.

Dooshta's picture

Thanks! Used your example to build a drag-and-drop table for one of my projects and got everything working without problems :)

Mohamed Badr's picture

it's working great, I used it in my accounting application,,,, thanks a million

Tory's picture

This tutorial was *perfect* for what I'm building. I've never implemented a custom theme function for rendering a custom form before, nor used drupal_render(), and this walkthrough is very clear. Love the modular approach that allows the rest of the form elements to be automatically rendered/themed as I'm used to. Thank you!!

Rob Malon's picture

When you're building the form, classes should be passed in as an array:

'#attributes' => array('class' => array('weight')),

Without it you'll get a warning:

Warning: array_merge(): Argument #1 is not an array in _form_set_class()

UKan's picture

Great stuff here. I was watching my code for over half an hour when I finally realized with your code that I forget to add this simple line $row['class'] = array('draggable'); ....

Anonymous's picture

Excellent example. Thanks for the hard work figuring out all of it :-)

Wayne Weibel's picture

applause for an excellent example ... i did hit an issue with the weight fields though. using simply drupal_render the weight field was not being rendered; it was failing or returning an empty string. either way, i ended up having to do:

$weight = array(
'#type' => 'weight',
'#delta' => 10,
'#value' => $value['weight'],
'#attributes' => array('class' => 'weight'),
);
drupal_render(process_weight($weight));

also, while it is not advised, the same effect can be accomplished within the form generation without 'needing' to implement the theme functions

Anonymous's picture

Hi Chadwick! This is a great tutorial, thanks! However it seems like it's running on Drupal 6? If so, do you know what are the changes if any that I'll need to make for Drupal 7? I've searched high and low and D7 dragtable examples are hard to come by! Thanks1

Chadwick Wood's picture

This tutorial is for Drupal 6, yes.  Sorry but I haven't had to try this on Drupal 7 yet, so I can't tell you what changes would be needed.

ingrid's picture

Some questions:
1. what if we just want to theme a segment of the form elements (drupal 6), like a set of textfields that we want to put into a table?
a. appropriate syntax
b. location of theme function? (in form module??)

ingrid's picture

ALSO* how does the module know to call function theme_example_form() ?

ingrid's picture

ALSO** what if we want multiple tables(filled with $form elements) of different sizes. Can we call multiple theme functions for the same form to build the elements into the theme_form($header,$rows)??

ingrid's picture

ALSO** (implicitly answered with answers to other q i ask) What if I'd like to make a theme_table() with form elements only in the last column?

ingrid's picture

ALSO** does the new $form['my_items'] = array(....) rendered by drupal_render($form) have to be called $form['my_items']?