January 12, 2011

Theme your Drupal Forms

Most of the time when you've written a Drupal form and it comes time to theme it, the majority of the work can be done using css. Things like settings forms, or configuration forms don't really need any special work and usually the default output of the form from Drupal is enough to work with. Just some css for the layout and form element sizes and you're done.

There is the odd time, however, when you need better control over the actual html output of your form. For example, when you want your form data to be displayed in a table. That is what this tutorial is all about.

A little bit of coding experience is expected in order to follow along in this tutorial. This tutorial was specifically written for Drupal 6, but I have a feeling Drupal 7 will use almost exactly the same method.

Overview

We are going to be writing our own simple module. We will define a form that will allow us to delete any number of selected nodes by checking a box and clicking a delete button (I know it's an arbitrary goal, but it's not about what it can do, it's about how it looks in the end). Following the form definition, we'll be writing a custom theme function that will render the necessary form elements into a structure that can be themed easily into a table.

Preparations

In your Modules folder, create a folder called delete_nodes and create two blank files:

  • delete_nodes.info
  • delete_nodes.module

That's all we'll need for now. The info file just needs the usual suspects added to it:

name = Delete Nodes description = Delete Nodes in the click of a button! core = 6.x

Then, enable the module and we're ready to go!

Writing the Form

First, we'll need to add a menu path for our form. As well I'm going to throw a permission because we don't want just anyone going in there and deleting nodes willy nilly

/** * Implements hook_perm(). */ function delete_nodes_perm() { return array('delete nodes'); } /** * Implements hook_menu(). */ function delete_nodes_menu() { $items = array(); //Defining a menu path and callback for our form $items['delete_nodes'] = array( 'title' => 'Delete Nodes', 'description' => 'Delete Nodes as you Please', 'page callback' => 'drupal_get_form', // The function used to 'page arguments' => array('delete_nodes_form'), // our form function 'access arguments' => array('delete nodes'), 'type' => MENU_CALLBACK, ); return $items; }

Next we have to add a function to define our form. Remembering that we actually going to be theming the output of the form ourselves, we need to make sure that we define the form in a way that the subsequent theme function we'll be writing can work with. Since we're writing both the form definition and the theme function, we actually have quite a bit of freedom here. Add this just below the delete_nodes_menu() function:

/** * The form for deleting nodes */ function delete_nodes_form(&$form_state) { global $user; // Our form array $form = array(); // This is what tells the form which theme function to use $form['#theme'] = 'delete_nodes_form_page'; // This will store all our node ids that we are displaying on the page // To be used as the options for the checkboxes form element $nodes = array(); $resource = db_query("SELECT nid FROM {node}"); while ($nid = db_result($resource)) { $node = node_load($nid); // Store the nid for our checkboxes 'options' $nodes[$nid] = ''; // We're just going to grab a few fields for now to make things simple // but this data could be anything from any database table // The important thing to note is that we're just putting this data // in the form as markup $form['title'][$nid] = array( '#type' => 'markup', '#value' => $node->title, ); $form['created'][$nid] = array( '#type' => 'markup', '#value' => date('M d, Y', $node->created), ); } // This is the checkboxes form element. // We're passing in the $nodes array which is just an array of the // Node nids that we looped through above. This will produce a checkbox // for each node $form['nodes'] = array( '#type' => 'checkboxes', '#options' => $nodes, ); // The delete button $form['delete'] = array( '#type' => 'submit', '#value' => t('Delete Selected Nodes'), ); // This will add some nifty highlighting for selected rows, // As well as provide us with access to a 'Select All' check box // in the table header. drupal_add_js('misc/form.js', 'core'); return $form; }

Theme the Form

The next step is to actually create the theme function for the form. We'll need register our theme function using hook_theme, as well as create the function itself.

/** * Implementation of hook_theme(). */ function delete_nodes_theme() { return array( // Our theme function 'delete_nodes_form_page' => array(), // You could use a actual template file instead of a function // Which would look like this: /* 'delete_nodes_form_page' => array( 'arguments' => array('form' => NULL), // Args to pass into the template file 'file' => 'aef.theme.inc', // If it's in a different file 'template' => 'aef-dynamic', // Template Name ), */ ); } /** * Theme function for the delete nodes form. * * Some fun stuff happens to your $form before being passed into * this function. Most importantly, you will find that the element * $form['nodes'] has been rendered into an array of individual * checkbox elements. This makes it possible to add each one individually * to each row! * * We're going to be using theme('table') to produce the actual table * which needs our data to be structured a particular way which you * will see below: * */ function theme_delete_nodes_form_page($form) { // Our header information $header = array( // This is the magic that creates the 'select all' box theme('table_select_header_cell'), array('data' => t('Node Title')), array('data' => t('Date Created')), ); // A variable to hold the row information for each table row $rows = array(); // element_children filters out any form properties from an array // (i.e. items whose keys have a '#' before them) and leaves us // with an array of the leftover keys // So, this next bit will loop through the available nids that we're // using to numerate our form data and create render out the data // to our table. To get an idea of what your form data looks like // uncomment the following lines (make sure you have the devel module on!) // dpm($form); // dpm($form['title']); // dpm(element_children($form['title'])); foreach (element_children($form['title']) as $key) { $rows[] = array(//row 'data' => array( //row data array( //Cell1 // $form['nodes'] now contains individual checkbox form elements! 'data' => drupal_render($form['nodes'][$key]), 'class' => '', ), array( //Cell2 'data' => drupal_render($form['title'][$key]), 'class' => '', ), array( //Cell3 'data' => drupal_render($form['created'][$key]), 'class' => '', ), ),//endrow 'class' => '', //row class );//endrow } // With our data all formatted nicely, we can just use theme_table // To produce our table $output = theme('table', $header, $rows, array('class' => 'delete_nodes')); // The function drupal_render() Keeps track of all rendered elements // And makes sure it doesn't render the same item twice. Running // drupal_render on the form at the end just renders what's left: // the delete button $output .= drupal_render($form); // And we return the output return $output; }

Hope that all made sense! I used the devel module to generate some nodes, and this is what it should look like so far if you go to www.yoursite.com/delete_nodes:

But wait! Nothing happens when you click the button! That's right, we still need a submit function for the form. I like to add my submit handlers just after the form function so add this right after delete_nodes_form()

/** * Submit handler for the delete_node_form */ function delete_nodes_form_submit($form, &$form_state) { // array_filter will remove any array items that evaluate to FALSE // Select only a few checkboxes and uncomment to see before and after: // dpm($form_state['values']['nodes']); // dpm(array_filter($form_state['values']['nodes'])); $nodes = array_filter($form_state['values']['nodes']); foreach ($nodes as $nid => $node) { // comment out node_delete() and uncomment // drupal_set_message() below to test // drupal_set_message(t('Deleting Node: ' . $nid)); node_delete($nid); } drupal_set_message(t('The items have been deleted.')); }

So there it is, your form has been generated and themed the "Drupal Way". I've attached the finished module as .zip file as well for anyone that just wants to download it and tinker around. Stay tuned next week for part 2 of this tutorial where we will add a confirmation step!

Download: delete_nodes.zip