Intro

Theming forms in Drupal 7 is mostly the same as Drupal 6, however there are some changes in how things are done in Drupal 7, and we will be exploring those changes in this tutorial. We will be working within a module called 'form_theme', and will create a form that looks like this:

My name is [FORM INPUT] [FORM INPUT] and I am [FORM INPUT] years old

The form inputs will initially display the words 'First name', 'Last name' and 'Age', which will then be removed when the user clicks on the input, and added again if the user clicks outside the input without entering a value.

What we will be covering in this tutorial:

  • Themeing a form
  • Attaching jQuery (JavaScript) to the form
  • Attaching CSS to the form

As the main goal of this tutorial is to show how to theme forms, I will not be going in depth into some of the other code that is not relevant to themeing. As such, to understand this tutorial, you will need an understanding of the following:

  • How to create a module in Drupal 7
  • How to create a form in Drupal using drupal_get_form()

I will also not be going into the explanations of the CSS and jQuery (beyond a few comments in the code), but you can copy and paste directly from my examples when following along with the tutorial. The reason behind this is that the CSS and jQuery, while necessary to show how they are added in Drupal 7, are not relevant to core purpose of this tutorial.

So let's get at it!

Step 1: Register a path to the form using hook_menu()

The first thing we need to do is register a path to our page that will display the form. I will not go into this in detail, but here is our implementation of hook_menu():

function form_theme_menu()
{
	$menu['form_theme'] = array
	(
		'title' => 'Theming Forms',
		'description' => 'Practicing theming forms in Drupal 7',
		'page callback' => 'drupal_get_form',
		'page arguments' => array('form_theme_form'),
		'access callback' => TRUE,
	);
	return $menu;
}

This definition registers the path form_theme at which we can access our form.

Step 2: Define the form

In our form, we will need three textfields, first name, last name, and age. The form definition for these is as follows:

$form['first_name'] = array
(
	'#type' => 'textfield',
);
$form['last_name'] = array
(
	'#type' => 'textfield',
);
$form['age'] = array
(
	'#type' => 'textfield',
	'#maxlength' => 3,
);

This is fairly straightforward. We have simply created the elements, and they are purely textfields, with no other markup or anything. We will be adding the text that separates them in the theme function later on.

Next, we need to attach our CSS and JavaScript to the form. Drupal 7 comes with a new attribute, #attached, in which we can add our scripts and stylesheets. We use do this as follows:

// Get the path to the module
$path = drupal_get_path('module', 'form_theme');
// Attach the CSS and JS to the form
$form['#attached'] = array
(
	'css' => array
	(
		'type' => 'file',
		'data' => $path . '/form_theme.css',
	),
	'js' => array
	(
		'type' => 'file',
		'data' => $path . '/form_theme.js',
	),
);

The benefit of adding the scripts in this method, rather than using drupal_add_js() and drupal_add_css(), is that the form can be altered by another module using hook_form_alter(), changing the stylesheets and scripts if necessary.

The last thing we will do is return our $form element. The entire form definition will look like this:

function form_theme_form($form, &$form_state)
{
	$form['first_name'] = array
	(
		'#type' => 'textfield',
	);
	$form['last_name'] = array
	(
		'#type' => 'textfield',
	);
	$form['age'] = array
	(
		'#type' => 'textfield',
		'#maxlength' => 3,
	);
	// Get the path to the module
	$path = drupal_get_path('module', 'form_theme');
	// Attach the CSS and JS to the form
	$form['#attached'] = array
	(
		'css' => array
		(
			'type' => 'file',
			'data' => $path . '/form_theme.css',
		),
		'js' => array
		(
			'type' => 'file',
			'data' => $path . '/form_theme.js',
		),
	);
	return $form;
}

Step 3: Register a theme function with hook_theme()

Since Drupal 6, theme functions need to be registered using hook_theme(). There are some slight differences in hook_theme() between Drupal 6 and Drupal 7 however. In Drupal 7, theme functions for forms don't use 'arguments', rather they use a 'render element' which only has one value, form. When registering the theme, we want to register it using the exact same name as the function that defined the form. Our form was defined in a function called form_theme_form(), so we will register a theme function called form_theme_form. Here is our implementation of hook_theme()

function form_theme_theme()
{
	return array
	(
		'form_theme_form' => array
		(
			'render element' => 'form'
		),
	);
}

As you can see, we have registered one theme function, form_theme_form which has one 'render element' passed to it, that is named form. Now the naming of the theme function is extremely important. Since the theme function has the same name as the form, this function will automatically be called when rendering the form. We don't need to add #theme to the $form element in our form definition, it all happens automatically. Finally, we will write our theme function.

Step 4: Write the theme function

In Drupal (7), there are a few things that need to be done in particular when writing a theme function. They are as follows:

  • The function is passed one argument, an array named $variables. Inside the $variables array is an element with a key called 'form'. All form elements live inside this element.
  • All form elements need to be passed through drupal_render(). This function is the magic that transforms the defined form element, which is really just a php array, into HTML, and attaches any javascript/css specific to that element. This all happens in the background though. All we need to do is pass the form element into the function.
  • At the end of the theme function, the remainder of the form should be passed through the function drupal_render_children to render any leftover and hidden elements (and all Drupal forms have required hidden elements which are necessary for the form to work properly). This is a major change from Drupal 6. In Drupal 6, we passed the form through drupal_render(), but in Drupal 7 this leads to an infinite loop, which causes the White Screen of Death, and no error messages to help us figure out what is going wrong.

Our theme function will be the text 'theme_' plus the theme name we registered in hook_theme(). Since we registered a theme named 'form_theme_form', our theme function will become theme_form_theme_form().

function theme_form_theme_form($variables)
{
	// Isolate the form definition form the $variables array
	$form = $variables['form'];
	$output = '<h2>' . t('Please enter your information below') . '</h2>';
	// Put the entire structure into a div that can be used for
	// CSS purposes
	$output .= '<div id="personal_details">';
	// Each of the pieces of text is wrapped in a <span>
	// tag to allow it to be floated left
	$output .= '<span>' . t('My name is') . '</span>';
	// Form elements are rendered with drupal_render()
	$output .= drupal_render($form['first_name']);
	$output .= drupal_render($form['last_name']);
	$output .= '<span>' . t('and I am') . '</span>';
	$output .= drupal_render($form['age']);
	$output .= '<span>' . t('years old.') . '</span>';
	$output .= '</div>';
	// Pass the remaining form elements through drupal_render_children()
	$output .= drupal_render_children($form);
	// return the output
	return $output;
}

This essentially completes the core of the tutorial. We have defined a form, registered a theme function, and themed the form using that theme function. However, we haven't added any CSS, so the form will not look the way I originally explained in the intro, and we haven't added any jQuery to show default text in the form elements, that disappears when clicked on, and is replaced when the form element is exited out of (if the element is empty). So we will get to that in the next section.

Step 5: Create the CSS and JavaScript files

In Step 2, we added CSS and JavaScript files to the form. These files however have not been created yet. When attaching them to the form, the given path was the root of the module, so these files will be added to the root of the module. First, the CSS:

#personal_details span, #personal_details div
{
	float:left;
}
 
#personal_details .form-item-first-name, #personal_details .form-item-last-name
{
	width:115px;
}
 
#edit-first-name, #edit-last-name
{
	width:100px;
}
 
#personal_details .form-item-age
{
	width:50px;
}
 
#edit-age
{
	width:35px;
}
 
#personal_details span
{
	margin-right:5px;
	padding-top:5px;
}

This stylesheet sets a width on the form elements, runs everything together by floating everything left, and adds spacing between the elements to make it look nice.

Next is the JavaScript (jQuery) file:

// We wrap the entire code in an anonymous function
// in order to prevent namespace collisions, and to
// allow for jQuery to be set in a safe mode where
// it will not collide with other javascript libraries.
// While this is the proper way to do it in Drupal 7,
// this method is actually good to use in Drupal 6 as
// well, for the same reasons.
(function($)
{
	// In Drupal 6, each element of Drupal.behaviors
	// was a function that was executed when the
	// document was ready. In Drupal 7, each element
	// of Drupal.behaviors is an object with an
	// element 'attach' (and optionally an element
	// 'detach'), which is executed when the document
	// is ready
	Drupal.behaviors.formTheme = {
		attach:function() {
			// First, define an empty array
			var defaults = [];
			// Next, add three elements to the array,
			// one for each of the form elements. The value
			// is of the array element is set as the default
			// text. This text is run through Drupal.t(),
			// which is the Drupal JavaScript equivalent
			// of the Drupal PHP t() function, and allows
			// for translating of text in a JavaScript document
			defaults["#edit-first-name"] = Drupal.t("First Name");
			defaults["#edit-last-name"] = Drupal.t("Last Name");
			defaults["#edit-age"] = Drupal.t("Age");
			// Next we loop through each of the elements of the array
			var element;
			for(element in defaults)
			{
				// We wrap the body in the following if() statement
				// as each element in an array will also have a
				// prototype element. If you don't understand this,
				// don't worry. Just copy it. It will make your
				// for(A in B) loops run better.
				if(defaults.hasOwnProperty(element)) {
					// 1) Set a placeholder in the form element
					// 2) Set the CSS text color to grey for the placeholder
					// 3) Attach an onfocus and onblur listener to each element
					$(element).val(defaults[element]).css("color", "grey").focus(function()
					{
						// This is entered on focus. It checks
						// if the value of the form element is
						// the default value of the placeholder,
						// and if it is, it clears the value and
						// sets the text color to black,as the
						// entered text will be the actual text
						// and not the greyed out placeholder text.
						var key = "#" + $(this).attr("id");
						if($(this).val() === defaults[key]) {
							$(this).css("color", "black").val("");
						}
					}).blur(function()
					{
						// This is entered on blur, when the element
						// is exited out of. It checks if the element
						// is empty, and if it is, it sets the default
						// placeholder text back into the element, and
						// changes the text color to the grey placeholder
						// text color.
						if($(this).val() == "") {
							var key = "#" + $(this).attr("id");
							$(this).css("color", "grey").val(defaults[key]);
						}
					});
				}
			}
		}
	};
}(jQuery));

And that's it. We now have a form that has been put into a theme, and had CSS and jQuery attached to it.