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$variablesarray 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_childrento 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 throughdrupal_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.

30 Comments
Thanks, this article made my day. I have translated it to Hungarian.
Cool!
This thing worked out for me, but not the way I wanted to.
I wanted to have all the elements in a TPL and then theme it. here in this case, all the work in being done from the Module.
There must be a method where we can do this from TPL level.
My search still on ...
I am also looking for a way to theme a form using a template (.tpl) rather than a function.
Just thank you!
- Some French guy
Thanks, you've just saved my brain from melting! Taken me hours to find someone saying 'use drupal_render_children'. Good guide :)
@Nathan: To use (.tpl) For Drupal 7, at step3, you can use:
'form_theme_form' => array(
'render element' => 'form',
'template' => 'TemplateFileName', // TemplateFileName.tpl.php
'path' => drupal_get_path('theme', 'nilecode').'/templates/forms', // path_of_theme_folder/templates/forms
),
Great post, you saved my day!
excellent tutorial very well described step by step :)
This was very helpful, and worked out for me - thank you! I found the naming of the example module, "form_theme" kind of confusing. Your example function names end up being like "theme_form_theme_form", and it's hard to see at first glance which part is the name of the hook and which is the name of the module.
Thanks Jaypan. Your a legend for taking the time to put this up. You almost replicated my exact problem.
I'm not sure you need that theme function in step 4 though. You can add HTML in the form building function with...
$form["ID"] = array(
'#type' => 'markup',
'#markup' => " some html ",
);
Was nice to see how to implement the theme hook function though and I'm sure I'll be using that very soon.
Another point... I used the css inline property to keep my elements inline whereas you used spans. I would love to hear your thoughts on the pros and cons of either method.
You can add the HTML in directly, but then it bypasses the theme layer making it more difficult to override for other designers/developers. And for this tutorial, it was half just to give an example of how to use the theme layer.
Also, you don't need to add "#type" => 'markup' to form elements, because the default type is markup.
I used spans, not to keep the text inline, but to give it a wrapper allowing it to be floated left. I prefer to float things rather than giving them a display of inline, because when you set the display of something to inline, you cannot set certain CSS properties to it (height, width etc). It's really a matter of style - if inline is working for you, keep with it.
Hi, Have you a idea to theming a altered form ? Ex:
function MY_MODULE_form_alter(&$form, $form_state, $form_id){ if.... } function MY_MODULE_theme() { return array ( 'MY_MODULE_form_alter' => array ( 'render element' => 'form', ), ); } function theme_MY_MODULE_form_alter($variables) { ... }
This code doesn't work. Have you an idea ? Thk :)
It was dark when I woke. This is a ray of sunihsne.
I tried what is there in the example above i am not able to get the output. iam attaching what i have done please let me know what is wrong in that
I want social buttons on your page to bookmark that article!
Very helpfull article and comments.
Especially thanx for explaining attaching JS right way (via Drupal.behaviors)
just started moving code from D6 to D7
got caught in a loop with drupal_render($form) while theming
your post saved me couple of hrs
thanks :)
Hi Jay,
Thanks for this awesome post! This perfectly works. However, I'm getting trouble when I call drupal_render - I posted a detailed question in stackoverflow, can you please check and kindly answer, if you get time? Thanks!
Link to question: http://drupal.stackexchange.com/q/26288/3235
Thanks a lot for sharing this! I didn't know about the #attached property - very nice.
Shafiul
I believe the problem is that you are calling drupal_render() and not drupal_render_children(). drupal_render() gets stuck in an infinite loop, since it eventually calls the function it exists in.
This is how all the tuts should be done, Thanks Jay
Happy to help!
Hi, Thanks for such a nice tutorial. Very descriptive and straight forward. Making life easier.... Keep on spreading the knowledge.
You missed a really important point.
The name of the hook_theme() implementation needs to be based on the name of the *module*, not the name of the form.
So, if the module is named "mymodule", the hook function has to be named mymodule_theme(), and it has to return an array listing *all* of the theme functions you wish to register.
Thank you very much. Be happy like me today ))
This is really helpful. A few minutes reading helps me save a day!
Jay -
Great post. Clears up a lot. You are very knowledgeable.
One thing that has confused me on forms is the form fucntion name. You have named your form function after the form hook (hook_form). But isn't hook_form used to create a form to edit a particular node? The arguements for hook_form ($node, &$form_state) are different from what you are passing to your function as well ($form, &$form_state) as $form is not a node, but an array, correct? Am I missing something simple? Can not the form function passed to drupal_get_form be any function name or must you use hook_form?
Thanks! and Thanks again for this great info.
thank you !!! you saved me!!!
Tom
You are correct, my code here would be an implementation of hook_form(), and can cause conflicts. I hadn't realized that, and will need to go back and change the code accordingly!
It is really good with simple english. Anyone coming from scratch can get through this concept. Good job..keep posting new concepts relevant to the drupal. Thankyou.
Add new comment