Multi-step Forms in Drupal 6 using variable functions

I recently had to write a multi-step form in Drupal 6. Of course, I turned to documentation to see how others are doing it. Pro Drupal Development offers the basics, so do the 5 to 6 upgrade notes , and others . I felt that many approaches suffered from design flaws that made the code cumbersome to manage beyond a couple steps. I set out to develop a multi-step form method with the following goals:

  • One form builder with nested conditional statements is difficult to manage, each step should be its own form array function
  • Steps shouldn’t be numbered, e.g. to move to the next step don’t $form_state['storage']['step']++
  • Each step should be able to have its own validate and submit handlers
  • Steps should be form alterable

As far as Form API knows, there is one form builder and thus one validation and submit function. The key is, each piece (builder, validate, and submit function) directs to sub-functions that perform during the appropriate step. Value elements are heavily used to control flow by informing the system what the next step is and for handling step validation and submit.

If you’re not too familiar with Drupal’s Form API watch this excellent intro video from my friend Chris Shattuck at BuildAModule.com

Let’s look at some pseudo-code to explain the main idea of what I’m doing.

We get started with drupal_get_form('main_builder')

function main_builder(form_array) {  
  Check if we've set our next step  

  If we have call that step's function and  
  return it's Form array  

  If we don't have a step than we're at the  
  beginning of our form  
  Call and return first_step()  
}

function first_step(form_array) {  
  Build the form elements we want the user to enter

  Define what the next step from this one is  
  form_array['next_step'] = 'a_single_step';  

  return form_array;  
}

function a_single_step(form_array) {  
  Build the form elements we want the user to enter  

  Define what the next step from this one is  
  form_array['next_step'] = 'next_step';

  return form_array;  
}

function main_builder_submit(form_array) {  
  Store submitted form values

  Set next step  
}

The advantage here is each step knows the next step and each step is contained within its own function, allowing for easy modification. The main form builder function dispatches to each individual step to build its own form array. The form submit handler stores submitted values in form storage per usual.

Let’s look at some Drupal code now.

<?php
// We call drupal_get_form('multistep_form')
// elsewhere, such as an implementation of hook_menu().
i
function multistepform_form($form_state) {
  if (!empty($form_state['storage']['step'])) {
    $function = $form_state['storage']['step'];
    return $function($form_state);
  }
  else {
    return _multistepform_form_start();  
  }
}

function _multistepform_form_start() {
  $form['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Name'),
    '#required' => TRUE,
  );
 

$form['continue'] = array(
    '#type' => 'submit',
    '#value' => 'Continue',
  );
  // Our special value elements.
  $form['this_step'] = array(
    '#type' => 'value',
    '#value' => 'start',
  );
  $form['step_next'] = array(
    '#type' => 'value',
    '#value' => '_multistepform_form_food',
  );
  return $form;
}

function _multistepform_form_food($form_state) {
  $form['food'] = array(
    '#type' => 'textfield',
    '#title' => t('Food'),
    '#required' => TRUE,
  );
  
  $form['continue'] = array(
    '#type' => 'submit',
    '#value' => 'Continue',
  );
  $form['this_step'] = array(
    '#type' => 'value',
    '#value' => 'food',
  );
  return $form;
}
function multistepform_form_submit($form, &$form_state) {
  if (empty($form_state['storage'])) {
    $form_state['storage'] = array();
    $form_state['storage']['values'] = array();
  }
  // Store submitted form values
  $this_step = $form_state['values']['this_step'];
  $form_state['storage']['values'][$this_step] = $form_state['values'];
 

// Set up next step.
 

if (!empty($form_state['values']['step_next'])) {
    $form_state['storage']['step'] = $form_state['values']['step_next'];
  }
  else {
    // Form complete!
    drupal_set_message(t('Complete.'));
  }
}
?>

See the full example in the attachment

As you can see, this method uses special form value elements to define the flow. The main builder uses variable functions to delegate which step it is. The second step, _multistepform_form_food() does not set a 'step_next' value and so on submission we don’t set a step that will be called when we return to the main builder. I use the ‘this_step’ value to namespace submitted values in form storage. Other than displaying the message “Complete!” I am not actually doing anything with the final, collected values yet.

The same delegation method using variable functions that the main form builder does can be applied to validate and submit handlers by creating value elements 'step_validate' and 'step_submit' with function names as the value in step form builders and the following main validate and submit addition:

<?php
function multistepform_form_validate($form, &$form_state) {
  if (!empty($form_state['values']['step_validate'])) {
    $function = $form_state['values']['step_validate'];
    $function($form, $form_state);
  }
}
?>

And this code in multistepform_form_submit().

<?php
  if (!empty($form_state['values']['step_submit'])) {
    $function = $form_state['values']['step_submit'];
    $function($form, $form_state);
  }
?>

Custom validation functions can form_set_error() normally. By specifying a step submit, our submit function can alter the form flow, skipping steps.

<?php
function _multistepform_form_like_music($form_state) {
  $form['like_music'] = array(
    '#type' => 'radios',
    '#title' => t('Do you like music?'),
    '#options' => array(
      0 => 'No',
      1 => 'Yes',
    ),
    '#required' => TRUE,
  );
  
  $form['continue'] = array(
    '#type' => 'submit',
    '#value' => 'Continue',
  );
  $form['this_step'] = array(
    '#type' => 'value',
    '#value' => 'like_music',
  );
  // New value, 'step_submit'.
  $form['step_submit'] = array(
    '#type' => 'value',
    '#value' => '_multistepform_form_my_submit',
  );
  $form['step_next'] = array(
    '#type' => 'value',
    '#value' => '_multistepform_form_music',
  );
  return $form;
}
function _multistepform_form_my_submit($form, &$form_state) {
  if ($form_state['values']['like_music'] == '0') {
    // If the user doesn't like music, well don't ask them anything more about it!
    $form_state['values']['step_next'] = '_multistepform_form_final';
  }
}
?>

In this example if the user says (s)he doesn’t like music (Who would choose that, really?) then instead of seeing the step _multistepform_form_music() (s)he gets sent to _multistepform_form_final().

I touched on the 'this_step' value element earlier, but it plays the part of identifying individual steps, allowing each step to be form alterable. Your implementation of hook_form_alter() could look for the multi-step form_id and if $form['this_step'] matches what you’re looking for. hook_form() may offer a solution, but I’m not certain it’s better or easier than using an additional value element.

After developing this method I was informed it is like Chaos tool suite&rsquo;s wizard. Other multi-step form approaches exist such as Pageroute , an object-based approach to steps of a flow.

The important pieces to use are variable functions and passing the $form_state array around by reference. For further exploration on this method I plan to see if it could be used to move backwards in a form, returning to previous steps. And the special value elements here are just a convention but I could see them being defined by hook_element().

Take a look at the attached .module for a full example spanning more than two steps. The code implements custom validation and a custom submit handler to alter form flow. If you would like to demo the code you’ll need a multistepform.info file. Further directions for module development is in the Drupal handbooks .

Update 07/11/2011 The Examples module for Drupal provides multi-step form directions and is a great resource