Theming Forms

Theming forms is a little different than working with the usual template file or theme function. Form markup is generated using Drupal's Form API. This makes it really easy for modules to build forms and guarantees consistency among generated elements. While the process of theming forms is quite different from what most front-end developers are used to, we think you'll begin to appreciate the consistency and flexibility of theming Drupal's forms.

One thing Drupal is famous for is the ability to accomplish a single task in many different ways. Although none of Drupal's forms ship with template files, they can easily be made to use them. Forms can also use preprocess functions, process functions, and alter hooks. So, how do you know when to use one over the other? This section will explain how forms are generated and will present a couple of examples using each method.

How Form Markup is Generated

Forms are generated by modules. The simple function shown in Listing 16–24 is all that is required to generate form markup. It looks really easy, doesn't it? It is. Of course, there is more to the process to make it functional, such as validating the form and saving the submitted values, but the rest is not your concern in the theme layer. What's important to you is the structure of a form and how it's transformed from the $form array to actual markup.

Listing 16-24. A simple unsubscribe form.

<?php
function exampleform_unsubscribe(&$form, $form_state) {
  $form['email'] = array(
    '#type' => 'textfield',
    '#title' => t('E-mail address'),
    '#required' => TRUE,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Remove me!'),
  );
  return $form;
}

In Listing 16–24, you define a very simple form with two elements: a textfield for the e-mail address and a submit button. When rendered, the result looks like those in Figure 16–5. The resulting markup is shown in Listing 16–25.

Screenshot of rendered unsubscribe form

Figure 16-5. Rendered form based on the code from Listing 16–24.

Listing 16-25. The markup generated by Drupal for the example form_unsubscribe() form in Listing 16–24.

<form action="/example/unsubscribe" method="post" id="exampleform-unsubscribe" accept-charset="UTF-8">
  <div>
    <div class="form-item form-type-textfield form-item-email">
      <label for="edit-email">E-mail address <span class="form-required" title="This field is required.">*</span></label>
      <input type="text" id="edit-email" name="email" value="" size="60" maxlength="128" class="form-text required" />
    </div>
    <input type="submit" id="edit-submit" name="op" value="Remove me!" class="form-submit" /> <input type="hidden" name="form_build_id" value="formjKkl1KLWJLnv0hM4DSVd8-40boTgBQAzWWhUn44c15Q" />
    <input type="hidden" name="form_token" value="LB07DqsDXK9idWdOHLxUen7jKxm52JqTyHiR7-pNumA" />
    <input type="hidden" name="form_id" value="exampleform_unsubscribe" />
  </div>
</form>

Form API Elements and Default Properties

In the exampleform_unsubscribe() form, you've defined two form elements: the e-mail address and the submit element. The e-mail element's #type property is textfield, which provides a single line text input. The submit element's #type is submit, which is the Form API equivalent of <input type="submit" />.

If you look closely at the generated markup in Listing 16–25, you'll see that you only set two properties in each element, but your markup ended up with some additional attributes. This is because Drupal assigns a default set of properties to each element. In this case, you are using form, textfield, and submit elements, which are defined in system_element_info(), as shown in Listing 16–26. When the form is processed, Drupal merges the properties defined in the form with the default properties.

Listing 16-26. Default element properties as defined in system_element_info() for textfield and submit elements.

<?php
$types['form'] = array(
  '#method' => 'post',
  '#action' => request_uri(),
  '#theme_wrappers' => array('form'),
);
$types['textfield'] = array(
  '#input' => TRUE,
  '#size' => 60,
  '#maxlength' => 128,
  '#autocomplete_path' => FALSE,
  '#process' => array('ajax_process_form'),
  '#theme' => 'textfield',
  '#theme_wrappers' => array('form_element'),
);
$types['submit'] = array(
  '#input' => TRUE,
  '#name' => 'op',
  '#button_type' => 'submit',
  '#executes_submit_callback' => TRUE,
  '#limit_validation_errors' => FALSE,
  '#process' => array('ajax_process_form'),
  '#theme_wrappers' => array('button'),
);
Tip: This form only touches on a few of form elements, but Drupal has many of them. For a full list of elements available through the Form API and their default properties, see http://api.drupal.org/api/file/developer/topics/forms_api_reference.html/7.

Rendering of Form Elements

The element properties contain critical information required to render them. Of these properties, two are very important in the theme layer: #theme and #theme_wrappers. When it's time to render the form, these properties tell Drupal which theme functions to use. There's also the option to use the #pre_render property to define a function(s) that should run prior to rendering.

#theme
Specifies the theme function to use when rendering the element.
#theme_wrappers
Specifies a theme function or functions that should be used to wrap the rendered children of the element.

To illustrate this process, let's use the $form['email'] field from the previous form and walk through the process:

Step 1: theme('textfield', array('element' => $form['email'])) is called. This results in the following markup:

<input type="text" id="edit-email" name="email" value="" size="60" maxlength="128" class="form-text required" />

Step 2:: theme('form_element', array('element' => $form['email'])) is called. This results in the following markup:

<div class="form-item form-type-textfield form-item-email">
  <label for="edit-email">E-mail address <span class="form-required" title="This field is required.">*</span> </label>
  <input type="text" id="edit-email" name="email" value="" size="60" maxlength="128" class="form-text required" />
  <!-- RESULT OF THE RENDERED TEXTFIELD -->
</div>

Finally, after all of the form elements are rendered, the form itself is run through theme_form(), which is specified as the #theme_wrappers in the form element. The theme_form() function takes care of generating the rest of the form markup, including the hidden elements form_build_id, form_token, and form_id.

Caution As mentioned previously, you never use theme_ to call a theme function directly, and similarly theme functions are entered in #theme and #theme_wrappers without the prefix theme_.

First Steps for Theming Forms

Find the Form ID

Before you can do anything, you'll need to find the ID of the form you're working with. It appears in the following two places in the markup of every form:

There's a hidden field near the bottom of the form named form_id that contains what you're looking for.

<input type="hidden" name="form_id" value="exampleform_unsubscribe" />

Although it's not copy/paste ready because it contains dashes instead of underscores to separate words, the <form>'s ID attribute also contains the form ID.

<form id="exampleform-unsubscribe">

Each Form ID has a corresponding function, which follows Drupal module naming conventions. In this example, exampleform is the module name and unsubscribe is what the form is named by the module.

Sometimes it helps to look at the original form and code comments when theming. You'll often find the original function that generates the form in the .module file of the module that created the form. If you find that the form doesn't exist in the .module file, it's definitely inside the module somewhere, but you may have to look around. Sometimes developers use .inc files for organization and code efficiency purposes.

Implement hook_theme().

In order to be able to use template files, preprocess, or process functions with forms, the first thing you'll need to do is register the form ID as a theme hook. This is necessary so that Drupal knows about the theme hook. Drupal core does this for some forms in core, mostly for administrative forms that use tables, but chances are you'll need to do this manually.

In your theme's template.php file, you'll create an implementation of hook_theme(), with your theme's name in place of the hook prefix. As an example, you'll theme the contact form located at /contact when the Contact module is enabled, whose form ID is contact_site_form. Inside you'll specify the form ID as the key and the render element as form, as shown in Listing 16–27. The render element key is required for theme hooks that use the render API to generate markup, such as forms. Its value indicates the name of the variable that holds renderable element, which in this case is form.

Listing 16–27. A hook_theme() implementation that defines the contact_site_form() theme hook as render element "form".

<?php
/**
 * Implements hook_theme().
*/
function THEMENAME_theme() {
  return array(
    // Defines the form ID as a theme hook.
    'contact_site_form' => array(
      // Specifies 'form' as a render element.
      'render element' => 'form',
    ),
  ); 
}

After doing this and clearing the cache, you'll be able to create a theme function and use preprocess and process functions for this form, which you'll get into later in the chapter.

Tip When registering theme hooks, if you are unsure what to enter, look at some of the default implementations. In this case, you are dealing with a form, so a quick look at http://api.drupal.org/api/function/drupal_common_theme/7 reveals the defaults for the original form theme hook, which are exactly what you need here.

Theming Forms with Theme Functions

The decision of whether to use a theme function or a template file is a personal/team preference. If you're comfortable using PHP, you might be inclined to use theme functions. If not, you'll probably prefer a template file, which is explained in the next section.

As discussed above, you'll need a hook_theme() implementation, without a template or path index, as shown in Listing 16–28. After doing this, HOOK_contact_site_form() is an official theme hook that can be overridden like any other theme function. Even though a theme_contact_site_form() function doesn't exist, you still name it as you would any other theme function override: THEMENAME_contact_site_form().

Listing 16–28. The basic required code for theming a form with a theme function.

<?php
/**
 * Implements hook_theme().
 */
function dgd7_theme() {
  return array(
    'contact_site_form' => array(
      'render element' => 'form',
    ),
  );
}
/**
 * Implements theme_forms_contact_site_form().
 */
function dgd7_contact_site_form($variables) {
  // Renders all elements of a form.
  return drupal_render_children($variables['form']);
}

Using drupal_render_children() Is a Must!

drupal_render_children() takes care of rendering all of the children of the form. This function alone will result in the exact same code Drupal would have provided without your theme function, which makes the function in Listing 16–28 pretty useless by itself, but it's worth stressing that it's VERY important to always use drupal_render_children($variables['form']) at the bottom of your function.

Even if you call render() on every element you have added to the form, Drupal will have added some important hidden elements identifying the form and those need to be rendered, too. So calling drupal_render_children($form) at the end of the theme function is mandatory. This won't re-print $form['foo'] because drupal_render() knows it has printed already. As an added bonus, it will take care of any additional elements added by other modules.

Manipulating Form Elements in Theme Functions

Now that we've gotten that out of the way, let's make some changes to the markup. Just like any theme function, the code this function returns will be inserted directly into the page markup. Since forms are render elements you need to render them. The code in Listing 16–29, does the following:

  1. Changes the labels of the name and mail elements.
  2. Renders the name and mail elements individually.
  3. Arranges the markup and individually rendered elements in a variable called $output.
  4. Includes drupal_render_children($form) in the $output at the bottom of the theme function.
  5. Finally, it returns the $output.

Listing 16–29. Implements theme_contact_site_form().

<?php
/**
 * Implements theme_contact_site_form().
 */
function dgd7_contact_site_form($variables) {
  // Hide the subject field. It's not required.
  hide($variables['form']['subject']);

  // Change the labels of the "name" and "mail" textfields.
  $variables['form']['name']['#title'] = t('Name');
  $variables['form']['mail']['#title'] = t('E-mail');

  // Create output any way you want.
  $output = '<div class="something">';
  $output .= '<p class="note">'. t("We'd love hear from you. Expect to hear back from us in 1-2 business days.") .'</p>';
  $output .= render($variables['form']['name']);
  $output .= render($variables['form']['mail']);
  $output .= '</div>';

  // Be sure to include a rendered version of the remaining form items.
  $output .= drupal_render_children($variables['form']);

  // Return the output.
  return $output;
}

Forms and their contents are render elements, so you can use hide(), show(), and render() functions to manipulate the elements of the form. When using hide() or making changes to the form array inside the theme function, you'll need to make sure you do so before attempting to render. There are a lot of other things that can be done here. We can't possibly cover all of them, but here are a few quick examples of what can be done:

  • Adjust the #weight property of an element to change the order in which they print. The following code would cause the message element to print at the top of the form:
$variables['form']['message']['#weight'] = -10;
$variables['form']['message']['#sorted'] = FALSE;
  • Add a description underneath an element by setting the element #description property, like so:
$variables['form']['mail']['#description'] = t("We won't share your e-mail with anyone.");
  • Set the default value of form element, such as checking the "Send yourself a copy" checkbox, by default setting the #checked property to TRUE, like so:
$variables['form']['copy']['#checked'] = TRUE;
  • Unset the #theme_wrappers property to remove the label and wrapper <div> and re-create the markup exactly the way you want it, like so:
unset($variables['form']['mail']['#theme_wrappers']);
  • More advanced changes include making the form display in a table by using the theme_table() function.

… and so on!

Tip: Using theme functions over templates is slightly faster performance-wise, but the difference is very minimal. Performance isn't something you should worry about when deciding whether to use a template file over a theme function unless it's for an element that can be used very frequently per page, such as theme_link().

Theming forms with Template Files

Creating template files for forms is surprisingly easy given what you've already learned. As mentioned in the "First Steps for Theming Forms" section, you'll need to open template.php and implement a hook_theme() function. Instead of just defining the render element, you'll need to add two more things, as shown in Listing 16–30:

  1. A path key (optional) that contains the path to where the template file is located in your theme.
  2. A template key that contains the name of the template file, without the .tpl.php suffix.
Caution: Template files defined this way are not auto-discovered. If the path is omitted, Drupal will only look for your template file in the root of the theme. Specifying the path of the template directory is only required if your file exists in a subdirectory of your theme.

Listing 16–30. hook_theme() implementation example for using templates with forms.

<?php
/**
  * Implements hook_theme().
  */
function mytheme_theme() {
  return array( 'contact_site_form' => array(
    'render element' => 'form',
    'path' => drupal_get_path('theme', 'mytheme') . '/templates',
    'template' => 'contact-site-form',
    ),
  );
}

After creating the hook_theme() function shown in Listing 16–30, you'll need to create the template file. In this case, it's located in the templates directory within your theme:

sites/all/themes/mytheme/templates/contact-site-form.tpl.php

Once that's complete, simply clear the cache and Drupal will begin using your template file.

If there's nothing in your file to begin with, you'll get a blank page where the form used to be. The first thing you should do is add this line back to the template file: <?php print drupal_render_children($form); ?>. This will get the entire form back, and even though you may not want to keep everything in the form, you need to print the contents of this at the bottom of the form to ensure everything works properly as detailed in the "Using drupal_render_children() is a Must!" section.

Manipulating Form Elements in Template Files

For the sake of covering this topic in detail, let's use the example from the Manipulating Form Elements in Theme Functions section. The code in Listing 16–31 represents the result of completing the following tasks:

  1. Changing the labels for the name and mail elements.
  2. Rendering the name and mail elements individually.
  3. Arranging your markup and individually rendered elements as you want them.
  4. Finally, printing drupal_render_children($form) at the bottom of the template.

Listing 16–31. contact-site-form.tpl.php implementation of the contact form.

<?php // Change the labels of the "name" and "mail" textfields.
$form['name']['#title'] = t('Name');
$form['mail']['#title'] = t('E-mail');
?>
<?php // Render the "name" and "mail" elements individually and add markup. ?>
<div class="name-and-email">
  <p><?php print t("We'd love hear from you. Expect to hear back from us in 1-2 business days.") ?></p>
  <?php print render($form['name']); ?>
  <?php print render($form['mail']); ?>
</div>
<?php // Be sure to render the remaining form items. ?> <?php print drupal_render_children($form); ?>

While there are slight differences, it's mostly the same (with less PHP). All of the possibilities that apply in theme functions apply just as well in template files. The variables themselves are slightly different. In theme functions and preprocess functions, the name element would be located in $variables['form']['name']. In template files, that same variable would be $form['name']. This is done specifically to make Drupal's monster arrays easier on template authors.

Caution Be sure not to hide or omit required form elements. In Drupal, presentation is totally separate from form processing. Drupal will expect those elements and prevent the form from being submitted if they are not filled in. These types of changes should be done in a hook_form_alter() implementation, using the #access property. See the "Modifying Forms Using Alter Hooks" section and Chapter 22 for more information.

Keep Your Template Cleaner with Preprocess Functions

In our example of theming a form with a template file, the template is quite messy. The definition of a clean template file is one that contains hardly any logic and that simply prints variables and maybe an occasional IF statement. If you are dissatisfied with the appearance of the template file, this is a perfect opportunity to use preprocess functions. To make this really clean, you'd do the following in a preprocess function:

  1. Perform all modifications to the form array.
  2. Create any new variables.
  3. Render each field individually and provide easy variables for templates.

Of course, this is not something you'd want to do on every form on your site. However, it's very useful and convenient for highly styled user-facing forms that you want to take extra care to get right, such as the login, registration, and contact forms. The process of doing this is very easy, as demonstrated in Listing 16–32 with the contact form.

Listing 16–32. Using a preprocess function to do the heavy lifting for the template.

<?php
/**
 * Implements hook_preprocess_contact_site_form().
 */
function mytheme_preprocess_contact_site_form(&$variables) {
  // Shorten the form variable name for easier access.
  $form = $variables['form'];

  // Change labels for the 'mail' and 'name' elements.
  $form['name']['#title'] = t('Name');
  $form['mail']['#title'] = t('E-mail');

  // Create a new variable for your note.
  $variables['note'] = t("We'd love hear from you. Expect to hear back from us in 1-2 business days.");

  // Create variables for individual elements.
  $variables['name'] = render($form['name']);
  $variables['email'] = render($form['mail']);
  $variables['subject'] = render($form['subject']);
  $variables['message'] = render($form['message']);
  $variables['copy'] = render($form['copy']);

  // Be sure to print the remaining rendered form items.
  $variables['children'] = drupal_render_children($form);
}

Because you've done all the work in the preprocess function, the template file in Listing 16–33 is crispy clean. Adding markup and classes and moving elements around is a piece of cake, and it's very easy to see what this template file does at first glance.

Listing 16–33. The result of using a preprocess function to provide a clean, minimal template for the contact form.

<p class="note"><?php print $note; ?></p>
<p><span class="form-required">*</span> <?php print t("Denotes required fields."); ?></p>
<ol>
  <li><?php print $name; ?></li>
  <li><?php print $email; ?></li>
  <li><?php print $subject; ?></li>
  <li><?php print $message; ?></li>
  <li><?php print $copy; ?></li>
</ol>
<?php print $children; ?>

Modifying Forms Using Alter Hooks

The ability of themes to use alter hooks is new in Drupal 7. Templates are great for situations where you want to have a lot of control over the markup itself, but there are quite a few situations where simply using hook_form_alter() can make things a lot easier, especially if you are comfortable with Drupal's form markup either by default, or in combination with changes you can make site-wide via theme functions. Using an alter hook is perfect for quick changes like:

  • Simple changes to form labels, descriptions, and other properties.
  • Changing the order in which the form elements print using the #weight property.
  • Wrapping a few elements in a <div> or <fieldset>.
  • Hiding or removing form elements that are not required.
  • Adding some markup to a form.

It's also arguably easier because there are fewer steps involved in the process. You don't need to implement hook_theme(). You also get full control over the elements. There are certain limitations to the changes you can make within theme functions, as it's already too late in the process.

  1. hook_form_alter(): Runs for all forms.
  2. hook_form_FORM_ID_alter(): Runs for a specific form ID.

There are reasons for using hook_form_alter() over hook_form_FORM_ID_alter() all the time, but those reasons mainly apply to the tasks a module developer needs to perform. Unless you are specifically targeting more than one form to do the same thing, as shown in Listing 16–34, it's probably best to use hook_form_FORM_ID_alter(), as shown in Listing 16–35.

Listing 16–34. Implementation of hook_form_alter() to target all or multiple forms.

<?php
/**
 * Implements hook_form_alter().
 */
function mytheme_form_alter(&$form, &$form_state, $form_id) {
  // Changes made in here affect ALL forms.
  if (!empty($form['title']) && $form['title']['#type'] == 'textfield') {
    $form['title']['#size'] = 40;
  }
}

Listing 16–35. Implementation of hook_form_FORM_ID_alter() to target a specific form.

<?php
/**
 * Implements hook_form_FORM_ID_alter().
 */
function mytheme_form_contact_site_form_alter(&$form, &$form_state) {
  // Add a #markup element containing your note and make it display at the top.
  $form['note']['#markup'] = t("We'd love hear from you. Expect to hear back from us in 1-2 business days.");
  $form['note']['#weight'] = -1;

  // Change labels for the 'mail' and 'name' elements.
  $form['name']['#title'] = t('Name');
  $form['mail']['#title'] = t('E-mail');

  // Hide the subject field and give it a standard subject for value.
  $form['subject']['#type'] = 'hidden';
  $form['subject']['#value'] = t('Contact Form Submission');
}