Theming Drupal Web Forms

Before. Standard Drupal form layout: labels above elements and form elements underneath each other.
After. Templated form layout with labels on the left and tabular form sections.

Designing complex but user friendly forms is challenging - let it be paper forms or online forms. In order to build good online forms you can choose any of several tools ranging from HTML editors to sophisticated systems, such as Adobe LiveCycle Forms. Content management systems (CMS), like Drupal lay somewhere in between the two extremes - they offer a bunch of tools for building forms, but often lack an user friendly UI for designing forms. Luckily there are some Drupal extensions to address this issue. Webform is pretty popular standalone module, but lacks any tools for designing form layout. Form Builder is up-and-coming project building a WYSIWYG form editor, but still in alpha stages and with no form processing. As a consequence, we need to prepare the layout using Drupal theming functions and templates. Since they seem pretty challenging at a first sight, I'll try to outline here how I tamed the system to produce the kind of forms I wanted.

Versions
Article was written for Drupal 6.

The forms in this example were done using Webform module, but since it gets converted into a Drupal Form API (FAPI) form, all of these theming functions should apply also for standard FAPI forms.

My sample form is an imaginary travel insurance application form. It's a one page form that could be easily restructured as a multipage form using pagebreak form component offered by Webform. Topmost on the form are the elements that affect the insurance price. Note that calculating the actual premium would be done both 1) on the fly on the client side and 2) once submitted on the server side in the "additional processing" step. Then there are the personal information fields for all the family members, and finally the standard confirmation box and marketing options. Note that depending on your needs, you could replace these personal information fields with compulsory registration prior entering the form. Registration form can be customized as well using built-in profile module.

By default Drupal lays out form elements underneath each other with labels straight above elements, and consequently the page grows long. We're going to re-group the elements and adjust the labels so that they're on the left from form elements. These actions not only cut the page length to less than one half, but also make it more readable. See pictures on the right and check out live sample insurance application form.

Form could be re-styled using CSS alone, but I decided to re-structure the whole form using tables, due to tabular information in children's details. In order to do this I created two helper functions, another one to format simple groups (fieldsets) and another one to format tabular groups. These tabular groups are represented as fieldsets (rows) containing other fieldsets (cells).

 

Theming Helper Functions

I created a separate file for all helper functions, so they can be easily shared between all the forms.

First the function for moving labels to the left:

/**
 * Replace a form fieldset (group) by a table formatted as:
 * +----------+--------------+
 * | name     | [input /]    |
 * +----------+--------------+
 * | ssn      | [input /]    |
 * +----------+--------------+
 *
 * @param $group - reference to the fieldset
 * @param $keep_fieldset - whether to keep the fieldset border, or to remove it
 * @param $weight - set group weight - heavier float down on the page
 */
function fieldset_labels_on_left_table( &$group, $keep_fieldset = false, $weight = 0 ) {
	// dpm($group);
	// confirm we get fieldsets, not form elements nor type-less elements behind page brake
	if( empty( $group['#type'] ) || $group['#type'] != 'fieldset' ) {
		return;
	}

	$rows = array();

	foreach( element_children( $group ) as $form_field_name ) {
		$description = $group[$form_field_name]['#description'];
		$group[$form_field_name]['#description'] = ''; // no duplicate desctiption below title
		// theme_form_element prepares label together with the required star
		$title = theme( 'form_element', $group[$form_field_name], '' );
		$group[$form_field_name]['#description'] = $description;
		$group[$form_field_name]['#title'] = ''; // we don't want the label to repeat
		$row = array(
			'data' => array( // row's cells live in data, option for attributes
				0 => array( // header cell's content lives in data, option for attributes
					// 'data' => $title,
					'data' => $title,
					'class' => 'label_cell'
				),
				1 => drupal_render( $group[$form_field_name] )
			)
		);
		$rows[] = $row;
	}

	$grp = array(
		'#type' => 'markup',
		'#weight' => $weight,
		'#value' => theme('table', array(), $rows)
	);

	// whether we replace the original fieldset or add the
	// reformatted content inside arbitrarily named sub-element
	if( !$keep_fieldset ){
		$group = $grp;	
	} else {
		$group['table'] = $grp;
	}
}

 

Then the other one that outputs sub-fieldsets laid out as a table:

/**
 * Replace a form fieldset (group) by a table formatted as:
 * +----------+--------------+--------------+
 * |          | name         | ssn          |
 * +----------+--------------+--------------+
 * | spouse   | [input /]    | [input /]    |
 * +----------+--------------+--------------+
 * | another  | [input /]    | [input /]    |
 * | fieldset |              |              |
 * +----------+--------------+--------------+
 *
 *
 * @param $group - reference to the fieldset
 * @param $header - set table's header cells' content
 */
function fieldset_subfieldsets_to_table( &$group, $header ) {
	// confirm we get fieldsets, not form elements nor type-less elements behind page brake
	if( empty( $group['#type'] ) || $group['#type'] != 'fieldset' ) {
		return;
	}

	$rows = array();

	foreach( element_children( $group ) as $group_name ) { // for each subfieldset
		$row = array( 'data' => array() );
		$row['data'][0] = array( // header cell's content lives in data
			'data' => $group[$group_name]['#title'],
			'class' => 'label_cell'
		);
		foreach( element_children( $group[$group_name] ) as $form_field_name ) {
			$row['data'][] = render_titleless( $group[$group_name][$form_field_name] );
		}
		drupal_render($group[$group_name]); //  sub-fieldset rendered it into void
		$rows[] = $row;
	}

	$group['table'] = array(
		'#type' => 'markup',
		'#value' => theme('table', $header, $rows)
	);
}

// helper function to remove header prior rendering form element
function render_titleless( $el ) {
	unset( $el['#title'] ); // title already in header, don't repeat in cell
	return drupal_render( $el ); // returns field without label
}

 

Form template

With the help of two above mentioned functions we can build the following kind of template for the form.

include_once( 'form-formatting-helpers.php' );

// headers shared by all tables
$personal_info_header = array( '', 'First name', 'Surname', 'SSN' );

// If editing or viewing submissions, display the navigation at the top.
if (isset($form['submission_info']) || isset($form['navigation'])) {
	print drupal_render($form['navigation']);
	print drupal_render($form['submission_info']);
}

// Print out the main part of the form.
// dpm( $form['submitted'] );

fieldset_labels_on_left_table( $form['submitted']['premium'], false, -50 );
fieldset_labels_on_left_table( $form['submitted']['personal_details'], true, -10 );
fieldset_subfieldsets_to_table( $form['submitted']['spouse'], $personal_info_header );
fieldset_subfieldsets_to_table( $form['submitted']['children'], $personal_info_header );
fieldset_labels_on_left_table( $form['submitted']['confirmation'], false, 50 );

// Always print out the entire $form. This renders the remaining pieces of the
// form that haven't yet been rendered above.
print drupal_render($form);

// Print out the navigation again at the bottom.
if (isset($form['submission_info']) || isset($form['navigation'])) {
	unset($form['navigation']['#printed']);
	print drupal_render($form['navigation']);
}

Note: just uncomment that dpm function (requires Theme developer module) to inspect the form structure.

Files

Now you're ready to experiment with the form. Naturally you can create your own form, but if you want to take a shortcut, just download the sample insurance form structure and import it into your Drupal as a starting point. (In order to import it you need Node export module.) In addition, to get started with these theming functions you have to copy the following two files into a correct place on your Drupal installation. If you're using Drupal out-of-the-box with default Garland theme, you'd need to create the following directories:

  1. sites/all/themes
  2. sites/all/themes/garland (Note that even if garland is builtin theme, all customizations to it should be placed under sites folder.)
  3. sites/all/themes/garland/insurance (Note: templates can be placed in any directory within the theme. This allows for better management and less clutter in the base level of the theme directory.)

When finished place the following files into that directory:

  1. webform-form-XX.tpl.php
  2. form-formatting-helpers.php

Just replace the XX in file name with the node id number. If you're using Pathauto module that hides node id the URL, you can find out the node id by hovering Edit tab, for example. After adding a new template you need to empty the Drupal cache, i.e. clear theme registry. This can be done more easily using Administration menu module: in the menu hover the site icon and click Flush all caches.

Further styling

You might have noticed in those templating functions that I added "label_cell" class to table header cells on the left. You can hook your own styles into these in the following fashion:

.label_cell {
	font-weight: bold;
	width: 120px;
	vertical-align: top;
}

There are good instructions for adding new stylesheets using theme's .info file. If you're using Garland theme, it's bit more complicated due to custom colors and generated CSS files, but you can manage it by 1) using CSS injector module (no extra CSS files) or by 2) creating a new sub-theme and copying color sub-folder.

Note that unlike template files CSS files cannot be located by default in any sub-directories. But if you're using your custom theme you can add that CSS rule directly into it or keep all form related CSS rules separated in their own file by importing that file in your custom theme's style.css:

@import url('/sites/all/themes/YOUR_THEME/insurance/insurance.css');

 

Recommended reading

I wrote other tutorials about theming Drupal comment forms and using Javascript with Drupal forms.

I also found the following articles really helpful:

32 comments

 
Luis #1 28 Mar 2010 22:53

Hi,
Very good tutorial!!. I have a problem and I dont know how to resolve. The file webform-form-12.tpl.php (12 is my ID form) dont render. With the devel module I know that use node.tpl.php and not the file that I created.

The 2 files (webform-form-12.tpl.php and form-formatting-helpers.php) are in my theme folder (sites/iaer/theme/mytheme/

I probe change the name to webform-form.tpl.php

Why dont recognizes??

Sorry my English

 
Juho #2 29 Mar 2010 21:25

Hi Luis, have you tried to clear theme registry?

 
funky2D #3 1 Apr 2010 17:21

Hi Juho,
I got 2 questions

1) I can't have this work cause i'm stack in the formweb tpl!
while calling this function on line 15; fieldset_labels_on_left_table( $form['submitted']['premium'], false, -50 ); for example!
How or where did u set the group 'premium' in your webform fieldset to group theme ?

2) what if i want something like 2 groupe

groupe1 (name, input) | groupe2 (name, input)

thanks in advance

funky2D

 
funky2D #4 2 Apr 2010 18:47

i got it, thanks anyway !
funky2D

 
Ryan #5 22 May 2010 14:14

After a few hours of research, I found your template sample and it's excellent - saved me a lot of time!

One very quick question, and feel free to tell me to leave you alone. Javascript... I'm trying to run a calculation on it, based on the number of people.

In the summary fieldset, Webform is renaming my fields and screwing up my JS work.

//
b = document.forms['LAYOUTFORM'].deposit_persons.value;
document.forms['LAYOUTFORM'].deposit_total.value = toDecimals(eval(a
* b));
//

Webform is making it "submitted[deposit_summary][deposit_persons]"

Any advice?

Again, thanks for the publish!

 
Juho #6 22 May 2010 17:49

Glad to hear that you liked the article!

When it comes to scripting Drupal web forms, I recommend you take
advantage of the jQuery library included in Drupal. Try to run
something like the following snippet in Firebug console to give you an
idea what works.

var a = $('#edit-submitted-persons').val();
$('#edit-submitted-sum').val( a * 100 );

Note that input fields generated by web form module have "edit-
submitted-" prefixed to the id, but anyway you can easily find out the
actual id:s by using Firebugs Inspect-function.

I recently wrote another article that demonstrates a new jQuery plugin for form calculations.

 
SR #7 22 Jul 2010 21:17

I have been trying to get your files to work with my own Webform for hours, but have not had any success.

I'm trying to use your "fieldset_labels_on_left_table" function with my own fields so they display inline like the "Gender" or "Premium" fields on your Travel Insurance Sample form.

For example, if I have a Webform with the form ID 123 and 3 fields (name, telephone and email), and I substitute the function calls in the webform-form-123.tpl.php sample file with the ones below, then all I get is a blank page:

fieldset_labels_on_left_table( $form['submitted']['name'], false, -50 );
fieldset_labels_on_left_table( $form['submitted']['telephone'], false, 0 );
fieldset_labels_on_left_table( $form['submitted']['email'], false, 50 );

I have even defined the "fieldset_labels_on_left_table" function inside the webform-form-123.tpl.php and commented the "include" statement, but that only produces a blank page as well.

If I remove the function calls above, and include some simple functions such as the ones listed below, then I get the expected output (the entire array or the name field are displayed):

return print_r($form);
OR
print drupal_render($form['submitted']['name']);

What am I missing here? What do I need to do to make this work with my own Webform and fields? Do I need to insert any code into my template.php file?

 
Juho #8 26 Jul 2010 21:15

If by a blank page you mean The White Screen of Death i.e. a completely blank page, it might be because some function has been declared twice, for example. I'd recommend you to take a look at that page and to enable logging as instructed.

Editing template.php is not necessary, just copy all the files mentioned here to the places defined above.

 
brack #9 12 Aug 2010 13:04

I copied the code from your tutorial, placed it in my theme folder ended up with two files as described. I edited webform-form-tpl.php in my theme folder fieldset_subfieldsets_to_table( $form['submitted']['donate'], $personal_info_header ); instead of others, and I see my form in form of table BUT there is NO INPUT FIELDS, inputs are missing, what did I do wrong?

 
Juho #10 15 Aug 2010 21:11

I don't remember having seen this happening. Could you post some details either here or by email and I'll have a look.

[edit]
Actually SR in the following comment points to a potential cause - maybe you're passing a field, not a fieldset to fieldset_subfieldsets_to_table?

Originally I had there in the beginning of that function this kind of check up to confirm it:

// precondition assert to confirm we're not fed form elements
assert( $group['#type'] == 'fieldset' );

...but in the end I had to give up on it to allow for multi-page forms.

Please, confirm that "donate" is really a fieldset containing several fields, and not a single field.

 
SR #11 16 Aug 2010 16:07

I got everything working using your files (webform-form-XX.tpl.php and form-formatting-helpers.php) AND your sample insurance form Webform. The reason I could not get it working before using my own Webform was that I had not defined a fieldset to include all of my sub-fields, and as a result your fieldset_labels_on_left_table() function was not placing my fields into an HTML table. I was also able to get your function to work using my Webform AFTER I created a fieldset and moved all of my fields under it. Thank you!

 
Juho #12 16 Aug 2010 21:22

Glad to hear you got it working. Thanks for sharing your solution!

 
Jenny #13 24 Aug 2010 06:40

Hi, your helper code is absolutely perfect... except for one thing. :)

I am using the "Select or Other" module (http://drupal.org/project/select_or_other) to create a dropdown with an "Other" option and textfield. That part works fine, but the label gets added back a second time on top of the select list. Plus, for some reason the "mandatory" star is only next to the label in the right cell, not in the left cell.

See screenshot: http://jenstechs.net/misc/webform_helper_labeloff.png

I don't know if this is something you have time to look into or can provide a hint for some place for me to look to fix this, but I would GREATLY appreciate it....

THANKS!

 
Jenny #14 24 Aug 2010 06:59

I should add - it wasn't clear in the screen shot, but the standard 'select' dropdown list works absolutely fine. It is only affected when I select "Include option for 'other'" (option added with the module I linked above). I don't know why they would be different...

 
Juho #15 24 Aug 2010 23:49

Hi Jenny, I had a quick look at the select or other source code, and it seems like it's adding both #process (line 47) and #theme (line 50) hooks to the form elements. (See also hooks at FAPI reference and form flow diagram.) I suppose these are causing the issue you see. I'll try to figure out later if there's a way to overcome this.

 
Jenny #16 25 Aug 2010 09:28

Thanks. That's sort of what I figured, though I wasn't sure where. I could tell they were both being called, because yours and theirs both executed, but it's the order that is being tricky - theirs is stealing the field-is-required star away from yours, and they also print the title (which your code is supposed to clear).

The visible title part I'm not worried about because I can hide it with CSS - but I am a little concerned about the field-is-required star not showing up in the left column... it should show up in both, and it's not...

 
Jenny #17 26 Aug 2010 09:09

One other teeny thing... is there any way to force the sub-table with headers to NOT use sticky headers?

 
sumit #18 5 Nov 2010 11:45

I tried implementing those two files in my theme directory but when i tried to view the form...it displays me the code of webform-form-XX.tpl.php file...please let me know where i am goin wrong...thanks

 
sumit #19 5 Nov 2010 16:13

i got it working...

 
Anonymous #20 12 Nov 2010 23:38

for a newbie, where do I place the function files and what do I call them?
In the node export (import) as a webform I get The import data is not valid import text and wonder if I am missing a beat.

Joe

 
Juho #21 14 Nov 2010 01:49

You need to be careful here since there are at least two Drupal modules that implement their own import functions:

  • Content Management > Node Export: Import: takes you to Node Export module (right)
  • Content Management > Content types > Import: takes you to CCK content_copy module (wrong)

I suppose you accidentally selected the second one, since this throws an error message: "The import data is not valid import text." You also need to disable any rich text editors for that text area, e.g. for FCKEditor you need to disable "admin/content/import.edit-code". If this doesn't help, let me know your Drupal, Webform and Node Export versions. I tested now that it works with Drupal 6.19, Webform 6.x-2.9 and Node Export 6.x-2.22.

 
Joe #22 15 Nov 2010 07:12

Yes, that makes a difference and I will play around with this. I'm still baffled by where the files need to be placed if done manually without the node export option. Files such as the functions and the above files you mention and the custom css file. So far I have the webform which can be viewed but without the styling as you have it.

 
Coolof #23 6 Jan 2011 08:43

Man, great tutorial, saved me a lot of time, thx!

 
winston #24 17 Jan 2011 03:04

Just a quick thank you. This worked extremely well for me.

One note. In spite of some of the comments above suggesting the correct solution (and of course the obvious name of the function!) somehow it still took me a few minutes to understand what was needed to get the table technique to work. For anyone else reading this thread the critical thing is you need to put a fieldset INSIDE another fieldset.

Example:
You want to collect in a table format as per some of the above examples the following information:
First Name, Middle Initial, Last Name, Phone Number
You want to allow up to 3 entries of this information. You'll need something like this...

- peopleinfo (outer fieldset)
+ personinfo (inner fieldset)
- firstname
- lastname
- middleinitial
- phone

You'll then refer to peopleinfo (the outer fieldset) in the fieldset_subfieldset_to_tables function provided.

 
Jon Tyler #25 7 Feb 2011 22:51

I tried for about 3 hours to get this working with my own form before I imported yours. Both forms break similarly: "Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 35 bytes) in /home/churchsw/www/dev7/includes/errors.inc on line 274"

Does this code work in Drupal 7?

 
Juho #26 8 Feb 2011 22:57

Sorry, it's still only Drupal 6 at the moment. I'll update it for Drupal 7 during the spring.

 
Jon Tyler #27 10 Feb 2011 15:06

Juho,

Thanks for the verification. I'll look forward to the update.

 
JJ #28 10 Feb 2011 12:55

Thanks for the tutorial. I was able to display my labels on the left but I was not able to was not able to remove the fieldset border. I set it to false but it still showed up. Only different thing is it that the fieldset is within another fieldset - Do you think that is the problem?

 
Anonymous #29 14 Mar 2011 09:58

It would help to know the version of webform you have used(I use v. 6.x-3.9). I have problems with overriding original behavior of webform. If I call it webform-form-40.php (the nid is definitely correct) it has no affect on the content, unless I add there a die() call. Themer info always says

Theme hook called:
node
File used:
sites/all/themes/garland/node.tpl.php
Candidate template files:
node-webform.tpl.php < node.tpl.php

 
Anonymous #30 14 Mar 2011 10:06

there was mistake in former comment, I called it : webform-form-40.tpl.php but this also doeas not work :(

 
Anonymous #31 14 Mar 2011 10:21

I found the bug :D I did not add there any fieldset sorry for bothering, nice tutorial :) thanks very much

 
mitch #32 22 Mar 2011 06:59

Hi..Has anyone had a go at porting this to Drupal 7? Love to use this if anyone has had any success.