The Form API in Drupal is a complex and powerful system that touches nearly every page in a Drupal site. Forms can be as simple as the search or login blocks commonly used. Or, they can be complex forms of interaction using #ahah, jQuery, and multiple steps to gather complex information while providing a usable and unique user experience. A common requirement for a multistep form is to have a different page title for each step of the form. This allows users to know what step they are on during a multistep process. In this article, we'll examine how FormAPI's #pre_render functions can make that happen. A normal multistep form implementation will look something like this: In site_join.module:


/**
 * Implementation of hook_menu().
 */
function site_join_menu() {
  $items = array();

  $items['join/%membership_type'] = array(
    'title' => 'Apply for a Membership',
    'description' => 'Application form for new memberships',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('site_join_application', 1),
    'access callback' => 'site_join_application_access',
    'access arguments' => array(1),
    'file' => 'site_join.application.inc',
    'file path' => drupal_get_path('module', 'site_join') . '/includes',
    'type' => MENU_CALLBACK,
  );

  return $items;
}

In includes/site_join.application.inc:


/**
 * Form API callback for the membership form.
 *
 * @param $form_state
 *   The current state of the form.
 * @param $membership_type
 *   The type of membership.
 */
function site_join_application($form_state, $membership_type) {
  if (!empty($form_state['storage']['step'])) {
    // We are beyond the first step of the form.
    $form = $form_state['storage']['step']['callback']($form_state, $membership_type);
  }
  else {
    // Start the form.
    $form = site_join_application_member_validation($form_state, $membership_type);
  }
  return $form;
}

/**
 * Form API callback for the member validation step of the application form.
 *
 * @param $form_state
 *   The current state of the form.
 * @param $membership_type
 *   The type of membership.
 *
 * @return
 *   A form API array.
 */
function site_join_application_member_validation(&$form_state, $membership_type) {
  // If we are on this form step, set the page title.
  drupal_set_title(t("Validate your membership"));

  $form = array();
  // Build your form in $form here.
  return $form;
}

This works really well, until you want to start reusing the form generation code as a smaller part of another form. Each form function needs to know if it should set the page title or not. So, we add a third parameter to our form function:


/**
 * Form API callback for the membership form.
 *
 * @param $form_state
 *   The current state of the form.
 * @param $membership_type
 *   The type of membership.
 */
function site_join_application($form_state, $membership_type) {
  if (!empty($form_state['storage']['step'])) {
    // We are beyond the first step of the form.
    // The form determines if the step should set the page title by
    // setting 'set_page_title' to TRUE.
    $form = $form_state['storage']['step']['callback']($form_state, $membership_type, $form_state['storage']['step']['set_page_title']);
  }
  else {
    // Start the form.
    $form = site_join_application_member_validation($form_state, $membership_type, TRUE);
  }
  return $form;
}


/**
 * Form API callback for the member validation step of the application form.
 *
 * @param $form_state
 *   The current state of the form.
 * @param $membership_type
 *   The type of membership.
 * @param $set_page_title
 *   Optional parameter to indicate that this form is the "primary" form for
 *   the page and should set the page title.
 *
 * @return
 *   A form API array.
 */
function site_join_application_member_validation(&$form_state, $membership_type, $set_page_title = FALSE) {
  if ($set_page_title) {
    drupal_set_title(t("Validate your membership"));
  }
  // The rest of your form function goes here.
}

Running this code will initially look to work fine. Unfortunately, it will break when your form throws a validation error. Drupal caches the output of form functions and uses the cached output when rebuilding a form that has failed validation. This means that site_join_application() is never called, and drupal_set_title() never gets a chance to override the page title on the rebuilt form.

#pre_render to the rescue!

Luckily, Drupal provides a few FAPI properties that will get called every time a form is built. One of them is #pre_render. #pre_render is called every time before an element is rendered with drupal_render(). Using a #pre_render callback, we can ensure that the page title is set when needed for any page.


/**
 * Form API callback for the member validation step of the application form.
 *
 * @param $form_state
 *   The current state of the form.
 * @param $membership_type
 *   The type of membership.
 * @param $set_page_title
 *   Optional parameter to indicate that this form is the "primary" form for
 *   the page and should set the page title.
 *
 * @return
 *   A form API array.
 */
function site_join_application_member_validation(&$form_state, $membership_type, $set_page_title = FALSE) {
  if ($set_page_title) {
    $form['page_title'] = array(
      '#type' => 'value',
      '#value' => t('Validate your membership'),
      '#pre_render' => array('site_join_form_page_title'),
    );
  }
  // The rest of your form function goes here.
}

/**
 * FAPI #pre_render callback to set a page title.
 *
 * We have to do this for multistep forms as when a field fails validation, the
 * form is pulled from the form cache. This means that we never get a chance to
 * call drupal_set_title(). We also can't do it from hook_menu()'s title
 * callback as hook_menu() doesn't know anything about the state of the form.
 *
 * @param $element
 *   The FAPI element who's value contains the page title to set.
 * @return
 *   The modified element that was passed in.
 */
function site_join_form_page_title($element) {
  drupal_set_title($element['#value']);
  return $element;
}

Even though the element hasn't been modified, remember to return it as the function calling #pre_render expects it. With this method, we can easily set the page title to any value by creating a #value element and setting its #pre_render to the site_join_form_page_title() function. Although it's easy to miss, FormAPI's #pre_render functions can be a powerful weapon in your page-tweaking arsenal.

Published in

Andrew Berry

Thumbnail
Andrew Berry is a architect and developer who works at the intersection of business and technology.

Featured Work

Latest Podcasts

Let's Connect

Want to learn more about working with us or just say hello?

Contact Us