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.