Tip: Using #pre_render in Multistep Forms

The Form API in Drupal is a complex and powerful system that touches nearly
every page in a Drupal site

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:

Get in touch with us

Tell us about your project or drop us a line. We'd love to hear from you!