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.
The goal
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.
Basics
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:
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:
As you can see, nothing groundbreaking here: just a "checkboxes" widget, and a submit button.
Storing data
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:
form_id
,op
, andform_token
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:
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.
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 form_id
.
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:
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 th.select-all
, I hear you asking ? Well this is the default class for a neat feature in Drupal 5 and later: a Javascript checkbox handling selection for all rows in the table where it belongs, and the checkboxes they can contain.
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.
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.
But $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:
Noticed the 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 drupal_render
:
At this point, we have every cell element in individual variables:
$index
contains the option value$checkboxTitle
contains the checkbox caption$box
contains 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:
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 !
Going further
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 forVariable editor
devel_variable_page
: shows an additional rendering trick, how to render a form with all blocks removeddevel_variable
: the form builderdevel_variable_submit
: the submit handlertheme_devel_variable
: the theme function for the form
coding standards, system_settings_form
When teaching things to new Drupal developers, it's a good idea to adhere to the coding standards.
Also, if you're going to store your data in variables, there's no need to use _submit with a bunch of unsets. The correct way is to use system_settings_form. A good example is found in the Drupal 5 conversion guide at the "Removed hook_settings()" header.
System_settings_form
I wouldn't enter the coding standards flame, so suffice to confirm that anything going to be submitted to core should follow the Drupal coding standards which you pointed to. I definitely agree on that, not because the Drupal coding standards are better (or worse) but because they exist and following the standards is more important in such shared work than the standards' intrinsic quality. But for all purposes, I find the OSInet indenting and bracing style much easier to follow, especially when explaining things: braces line up vertically and make blocks stand out more visibly, which is supposed to help anyone trying to understand things, which is the purpose of this post.
Regarding
system_settings_form
, you probably noticed I mentioned it before showing how to do it "by hand". It's a neat function, which I use all the time for my own settings form, but not all forms are settings forms, and thus amenable to the "system settings" layout, especially considering the two buttons, their non-customizable wording, and the rigid storage model, which doesn't necessarily fit the needs of all forms. So I think you cannot really say it is , but just one way: keep in mind this is a tutorial, not something you'd want to use unchanged.What do you do next?
I've followed your steps - created mymodule.info and mymodule.module files, and implemented them. Now what happens? I can't see any reference to my new module anywhere but the modules page. How do I use this module?
Main menu
Once the module is enabled, you should see a "Form demo" link appear in your Navigation block. Click on it to see the form with its checkboxes.
I'm a lonely drupal coder
CPU is not the limit
Hi, "anonymous".
I enabled the php formatter on your comment, reformatted it, and added a few missing braces to make your code valid.
In typical Drupal setups, the CPU is not the limiting factor: the DB typically is, especially on the write side since MySQL is very efficient on reads. And in that case you're doing
variable_set()
in a loop, which means up to n+p writes where n is the number of checkboxes and p the number of selected checkboxes.Also note that, when developing complex forms, bypassing FormsAPI like this is likely to let you write more vulnerable code, or code that takes longer to write and in most cases your time will be more expensive than the extra CPU/DB resources possibly needed by using "standard" APIs, not even mentioning the increased cost of ownership since this means more custom code, written in a unusual fashion, which the maintainer will need more time to grok.
More generally, to discuss such topics, you should use the forums or #drupal-dev or #drupal channels rather than comments on a blog: there core devs would typically answer you in more detail, and I'm not one: I have only a few patches in core.
Thanks for the response. I
Thanks for the response. I posted this same question on drupal site http://drupal.org/node/335172
With drupal form API, do you know what's the least intrusive way to reformat the check boxes or radios, so they appear on the same line, instead of spanning multiple lines?
Using your example, it would be like: "x" a "x" b "x" c, instead of
"x" a
"x" b
"x" c
thanks!
Class container-inline
You'll typically want to apply class
container-inline
to the container for your elements. This is a standard Drupal class which essentially says itsdiv
andlable
children havedisplay: inline
instead ofdisplay: block
.See system.css for the actual implementation (link is to D7, but this exists since D5. In D4.7 it was in
drupal.css
instead.How to find out the checked Checkbox from a list of checkboxes?
Hi,
Pls provide a way to find out the selected checkboxes from the list of checkboxes. Looping through the values and if it is checked then processing that record.
Thanks..
"It depends"
foo_form_submit($form, $form_state)
for a form calledfoo_form()
, you'll typically just loop on$form_state['values']
, which will hold the data you need.automagically... nothing happens without a hook_theme
Hi,
thanks for this really useful and clear tutorial.
I found that in Drupal 6 it only worked if I also created a hook_theme function. In your example it would be:
After that, everything works just as you say it does. Thanks very much!
Cant get it to work
This is killing me.. I cant use the module there is no link anywhere. How am I supposed to use this, where should the link appear do you need to create a page or it create it itself.
I got one question. Is there
I got one question. Is there a way that you can add everything just created (a themed form) to a fieldset. I want to make a module where the user can navigate easily, so collapsing is very important.
Thanks for sharing!
Needs info
@drupal_phobic: Sorry, I don't even understand your question: this is a tutorial to explain how to write your own FAPI code, not a module you can use out of the box, so there is nothing to click around. What exactly are you asking ?
Fieldsets
Fieldsets have zero impact on the submitted data. Technically, they are not "input" widgets for FAPI, so you can just embed the array holding these checkboxes as a child element of a fieldset element, and this will work.
This is exactly how the page at admin/build/modules is built, to allow grouping the packages instead of having one massive module list. And admin_menu module even allows automatic folding for these fieldsets.
Cutting and pasting is not that difficult, but...
... it would be nice however if the possibility do download the module as a .txt file worked :-(
Download
Funny: apparently the path to the attachment was borked in the D5 to D6 upgrade: the
files
directory appeared twice in the automatic download link. Works now. Looks like anupload_update_N()
bug, though.So cool, thanks.
Just to had insert a :
in
form_demo_form_submit()
for Drupal 6 and it just worked!For Drupal 6 users you must also change this:
The menu must be like this:
You must also change function
form_demo_form_submit()
so it will be like this:Thanks for porting my example to D6 :-)
I took the liberty to fix some formatting issues in your code, so it is easier on readers. Specifically, form handlers in Drupal 6 FAPI always receive a
$form, $form_state
pair of arguments, and the old$form_values
is for most purposes replaced by$form_state['values']
.Drupal 6 changes
Some changes I had to make to use this code in Drupal 6:
1. When you set the #default_value of an element of type checkboxes, the default value has to be an array. Accordingly, the second parameter of variable_get should be 'array()', rather than '0'.
2. The function signature for hook_form_submit has changed to (&$form, &form_state). So the value you want to save is at $form_state['values'][name of field].
Other than that, this was really helpful. Many thanks!
Want to submit a revised version ?
Hi Gbirch,
Thanks for your comments. I suspect many readers would appreciate a fully revised version for D6 which they could just cut and paste. If you feel like it ;-)
Still Helpful
Many years later, this tutorial is still very helpful, and was easily ported to D7. Thank you.
Drupal 7 version ?
Hi Gale, thanks for your comment.
If you feel like adding some notes on what you had to change to use it on D7, I'm sure a number of visitors would appreciate it..