For some reason or another, I've noticed several new Drupal developers these last few days sweating on Forms API, and thought it would be nice to have a smallish example to complement the unavoidable FAPI reference and Guick start guide, for a typical non-basic form: one including set of checkboxes in a table, with a customized display, like the core user, content or modules administration forms. So follow me while we build this example.
We want to have a form containing a set of rows, displaying a checkbox with its label and the actual value stored when it is checked.
Oh, and we want it reversed. That is, the checkbox label will be on the right on a western display, not on the left as they are by default, and without using RTL tricks.
First we'll need an info file. I'll leave it to you; just don't forget it.
And a module file. And we'll have to enable them. D'oh !
The start of our module is unremarkable, except for the fact that we'll be following the OSInet coding style, not the standard Drupal style, for better readability:
* Demonstrate a few mildly advanced topics with Forms API
* @version $Id$
* @author FG Marand
* @license GPL 2 or later
$items = array
'path' => 'form_demo',
'callback' => 'drupal_get_form',
'callback arguments' => array('form_demo_form'),
'access' => user_access('access content'),
'title' => t('Form demo'),
Building the form
So, as we've seen in
form_demo_menu, we use
drupal_get_form as the callback to invoke our actual form builder function. Nothing revolutionary, this is typical form generation:
* Just a simple form
* @return array
$form = array();
$form['fd_boxes'] = array
'#type' => 'checkboxes',
'#options' => array
15 => 'a',
20 => 'b',
25 => 'c',
'#default_value' => variable_get('fd_boxes', 0),
$form['submit'] = array
'#type' => 'submit',
'#value' => t('Test'),
As you can see, nothing groundbreaking here: just a "checkboxes" widget, and a submit button.
Now we need to store the data from this form, which is not done by default if we are not using
system_settings_form. A quick check to the FAPI reference and guide show us we can simply implement a
form_demo_submit function implementing this storage, and it will take two parameters: the
form_id and the
form_value containing our actual data.
A quick look at
$form_value shows us it only contains the actual keys we need to store, not the captions of the checkboxes, which is quite fine. What it does show us, thought, is a few other elements:
- Our own submit button
- Three builtin elements from FAPI:
Although all of these are necessary, we certainly don't want to store them, so we can safely unset their keys from the
form_values array, since it is passed by value:
function form_demo_form_submit($form_id, $form_values)
* Look at form_values: it only contains the keys to the fd_boxes array:
* We're not supposed to use the captions for storage
// Exclude unnecessary elements. This one comes from our form:
// But these ones are put in by Forms API and always there
unset($form_values['form_id'], $form_values['op'], $form_values['form_token']);
// To be continued ...
At this point, our submit handler only sees the actual elements to be saved, which in this case means the array row containing the selected options for our
fd_boxes set of checkboxes. Let's store it.
// ... continuing
foreach ($form_values as $key => $element)
variable_set($key, $element); // Serialized as necessary, so it will store an array
Of course, we could have just saved it directly and ignored this unsetting and looping, but by coding it that way, you'll be able to reuse that piece of code unchanged.
Theming the form
How it works
The FAPI Quick start guide touches only lightly on the subject, but this is the actually interesting/unusual part: FAPI uses the theme layer to display forms, and will automagically invoke
theme_form_demo_form to render a form bearing
form_demo_form as its
So how does it work, from the application programmer's point of view ? Well, the key function here is
drupal_render. It will recursively render the form element we pass it, and flag the element with the FAPI-internal
#printed property to avoid rendering it another time if it comes to be invoked again. In effet, once an element has been drupal_render'ed, it's as if it was no longer in the form array.
The CSS touch
By default, table and checkbox display in most themes won't fit our needs, so we'll start by adding some local CSS for this specific form. Since it's only for one demo form, there's no need to put it in the CSS handling system:
* First we need to override some builtin styles for our form table:
* - the 100% width on tables
* - the display:block on form-item divs containing checkboxes
width: auto; /* Remove the 100% width default in many themes */
We'll apply class
special-checkboxes to our table, which make a perfect CSS selector for the table. The checkboxes are contained in DIVs with class
form-item, which by default has
display: block, which we need to override.
The JS touch
And what about
So it is time to activate it in the header for our table, which we build according to the
theme_table specification, and declare our rows array. Just two column: one for the values, the other for the checkboxes and their labels on the left.
$arHeader = array
t('The option values'),
$arData = array();
Building the table rows
And since headers only get us so far, it's time for the actual data.
theme_table uses the data as an array or rows, in which each row contains the individual cells. The cells, in turn, are either plain strings and displayed as such, or arrays themselves, in which the row indexed by key
data is the string to be displayed, and other rows are passed as attributes of the TD element.
For this, we'll want to render/theme all the elements resulting from the
checkboxes widget we declared, and which doesn't happen to exist in (X)HTML.
$form['fd_boxes'] contains a ton of properties in addition to the data we actually need display, namely its individual children checkboxes and their labels and values !
This is where
element_children comes in : it will return the keys to the children of any given form element, without the properties, which in this case are just "noise" for us. We can then loop on its result, and manually display the individual checkbox titles and the options values, which happen to be the keys to the children of the
checkboxes form element:
* Loop on element_children, not on all array keys: we are not interested
* in properties, only elements which are children of fd_boxes
foreach (element_children($form['fd_boxes']) as $index)
$checkboxTitle = $form['fd_boxes'][$index]['#title']; // get box title
unset($form['fd_boxes'][$index]['#title']); // then remove it
unset ? This is how we remove the default caption on the checkbox, to prevent it from being displayed on the right of the checkbox as any normal would.
Now we need to ask drupal to render the box in the table row we are building, using
$box = drupal_render($form['fd_boxes'][$index]);
At this point, we have every cell element in individual variables:
$indexcontains the option value
$checkboxTitlecontains the checkbox caption
$boxcontains the checkbox widget itself, i.e. the (X)HTML INPUT element
And we can fit it all together to build one table row and add it to the rows array:
$row = array // the row itself
$index, // its first cell, showing the actual option value
array // its second cell, showing the reversed checkbox
'data' => "$checkboxTitle$box",
'style' => 'text-align: right',
$arData = $row;
That ugly inline styling ? It's just here to remind you of the fact that any cell in the data rendered
theme_table can be a plain string (like
$index for the first column, or an array as we are using for the second column.
Now we can finally hand over all these data to
theme_table and invoke
drupal_render once more to render the elements we didn't specifically theme. In this case this will mean the submit button. Remember how it knows what has already been rendered by using the
#printed property ? This is where is proves to be useful !
// Now render the table representing fd_boxes
$ret = theme('table', $arHeader, $arData, array('class' => 'special-checkboxes'));
// And do not forget to render the rest of the form
$ret .= drupal_render($form);
There's nothing more to it. You can find the source code for the full module attached to this page.
Note that this example has been built and tested on Drupal 5, but should work with little difference on Drupal 6 and Drupal 7.
For a more complex, production quality, example built on this idea, have a look at devel.module, from which the idea for this example came, and specifically the following functions which make up the "Variable Editor" form:
devel_menu: look for
devel_variable_page: shows an additional rendering trick, how to render a form with all blocks removed
devel_variable: the form builder
devel_variable_submit: the submit handler
theme_devel_variable: the theme function for the form