How to create a multi-step form in Drupal 7

In this blog I'm going to show how you can create a simple multi-step form in Drupal 7 with forward and backward navigation, per-step validation and how you can retain values while moving between steps.

You can download the complete module to look through the full code as we go, and also view the module in action to get a better understanding of whats happening.

This tutorial assumes you have a baisc understanding of Drupal forms. If not I'd advise taking a look at the Drupal Form API before hand to understand how baisc forms work.

In this example I'm going to create a simple 3 step survey form in a module called 'customer_survey'. First of all I am going to create a 'master' form, which will pull in the specific form elements for each step depending on which step the user is on.

Set up the Form

function customer_survey_form($form, &$form_state) {
      
  drupal_add_css(drupal_get_path('module', 'customer_survey') . '/css/customer_survey.css');
  if (!isset($form_state['stage'])) $form_state['stage'] = 'rate_the_room';
  
  $form = array();
  $form = customer_survey_get_header($form, $form_state);
 
  switch ($form_state['stage']) {
     
    case 'rate_the_room':
      return customer_survey_rate_the_room_form($form, $form_state);
     break;  
  
    case 'rate_the_service': 
      return customer_survey_rate_the_service_form($form, $form_state);  
     break;
  
    case 'personal_details': 
      return customer_survey_personal_details_form($form, $form_state);  
     break;
  
    default:
      return customer_survey_rate_the_room_form($form, $form_state);
     break; 
  }
   
  return $form;
     
}

As you can see above - firstly I check if there is a currently a 'stage' set, if not I set it to the first stage.

if (!isset($form_state['stage'])) $form_state['stage'] = 'rate_the_room'

Next I initialise the form array, and call a function to populate the heading section - this is something we'll come onto later.

Finally I have a switch statement which - based on the current stage - calls a function which will return that particular stage's form, which you can see below.

function customer_survey_rate_the_room_form($form, &$form_state) {
     
  $values = isset($form_state['multistep_values']['rate_the_room']) ? $form_state['multistep_values']['rate_the_room'] : array();
 
  $form['rate_the_room']['room_rating'] = array(
    '#type' => 'radios',
    '#title' => 'How would you rate the room you stayed in?',
    '#options' => array(1 => 1, 2 => 2, 3 => 3, 4 => 4, 5 => 5),
    '#default_value' => isset($values['room_rating']) ? $values['room_rating'] : NULL,
  );
   
  $form['next'] = array(
    '#type' => 'submit',
    '#value' => t('Next')
  );
     
  return $form;
}

As you can see, first of all I populate the values variable with previously saved data from this step (we'll go through this later), so that if a user has come to this step back from another step, their data is preserved. Then I've set up a simple radio button form for the user to rate their room.

Validate the Steps

function customer_survey_form_validate($form, &$form_state) {
     
  if ($form_state['triggering_element']['#value'] == 'Back') {
    return;
  }  
     
  switch ($form_state['stage']) { 
       
    case 'rate_the_room':
      return customer_survey_rate_the_room_validate($form, $form_state);
     break;  
  
    case 'rate_the_service': 
      return customer_survey_rate_the_service_validate($form, $form_state);  
     break;
 
    case 'personal_details': 
      return customer_survey_personal_details_validate($form, $form_state);  
     break;
  
  }
}

The validation stage works in a similar way to the initial master form in that we've split out the validation into a separate function for each step to make it easier to track. A key feature at the top of this function though is that we don't want to validate if a user has moved backwards through the form - only when they've moved forwards.

Submit the Form

The submission stage is where most of the complicated stuff happens. Once again we run a switch statement on the step so we can do different things for different steps - but for this particular form the only stage in which we want to save anything is after the final one.

function customer_survey_form_submit($form, &$form_state) {
   
  switch ($form_state['stage']) {
     
    case 'personal_details':
      $form_state['multistep_values'][$form_state['stage']] = $form_state['values'];
      if ($form_state['triggering_element']['#value'] != 'Back') {
        customer_survey_personal_details_submit($form, $form_state);
        $form_state['complete'] = TRUE;
      }
     break;
  
    default:
      $form_state['multistep_values'][$form_state['stage']] = $form_state['values'];
      $form_state['new_stage'] = customer_survey_move_to_next_stage($form, $form_state);
     break;
  
  } 
 
  if (isset($form_state['complete'])) drupal_goto('complete-page');   
   
  if ($form_state['triggering_element']['#value'] == 'Back') {
    $form_state['new_stage'] = customer_survey_move_to_previous_stage($form, $form_state);
  } 
   
  if (isset($form_state['multistep_values']['form_build_id'])) {
    $form_state['values']['form_build_id'] = $form_state['multistep_values']['form_build_id'];
  }
  $form_state['multistep_values']['form_build_id'] = $form_state['values']['form_build_id'];
  $form_state['stage'] = $form_state['new_stage'];
  $form_state['rebuild'] = TRUE;
     
}

Ok so there are quite a few points to go through in this section. As you can see for the submission of 'personal_details' we call a function which will deal with the actual submission of the final form - and then we set the form to complete, and go to a complete page.

For all the other pages we store the values in the form_state and then call a function which will work out which the next stage is (we'll come to this later). However further down if we find out the form was submitted using a back button - we call a function to override this value by working out which the previous step was

Finally we have a bit of code which stores the form_build_id. A strange thing happens with these multistep forms whereby after your second submission Drupal rebuilds the form_build_id meaning if you refresh the form at any point you get taken back to the second step. This way we always keep the same form_build_id - and stay on the steps we want to be on.

Finally we store what the next stage will be and set rebuild to true so the form will reload with its form_state in tact - meaning that our initial switch statement in the customer_survey_form function will catch our new stage!

Navigate the Form

With multi-step forms it's always nice to let the user know how many steps there will be, and how many they have completed. In the first function we created there was a line of code which called the function customer_survey_get_header). We're going to look at that now.

function customer_survey_get_header($form, &$form_state) {
   
  $form_state['stage'] = isset($form_state['stage']) ? $form_state['stage'] : 1;  
     
  $form_stages = array(
    'rate_the_room' => 1,
    'rate_the_service' => 2,
    'personal_details' => 3,
  ); 
     
  if (isset($form_stages[$form_state['stage']])) {
   $current_step = $form_stages[$form_state['stage']]; 
  }
  else {
   $current_step = 1;
  }
   
  $stages = array(
    1 => array('data' => '1. Rate the Room'),
    2 => array('data' => '2. Rate the Service'),
    3 => array('data' => '3. Enter the Draw'),
  );
   
  $stages[$current_step]['class'] = array('active');
   
  $stages_list = theme('item_list', array('items' => $stages));
   
  $form['header'] = array(
    '#type' => 'fieldset',
    '#title' => '',
    '#value' => $stages_list,
  );
 
  return $form;
   
}

In this function we work out which step the user is currently on, and returns them a list of steps with their current step given the class 'active'. This way we can style the block to highlight their current step.

Finally we're going to look at customer_survey_move_to_next_stage() and customer_survey_move_to_previous_stage() which are called in customer_survey_form_submit(). In this example the code is very basic. It just moves the step forwards or backwards. But in more complicated multi-step forms this might be used to calculate if an additional step needs adding based on previous results.

function customer_survey_move_to_next_stage($form, &$form_state) {
 
  switch ($form_state['stage']) {
    case 'rate_the_room':
      return 'rate_the_service';
     break;
 
    case 'rate_the_service':
      return 'personal_details';
     break; 
  }
 
}
 
function customer_survey_move_to_previous_stage($form, &$form_state) {
 
  switch ($form_state['stage']) {
    case 'rate_the_service':
      return 'rate_the_room';
     break;
 
    case 'personal_details':
      return 'rate_the_service';
     break;
  }
     
}

And there we have it. As I said at the top you can download the full working module and look though the code and alter and pull it apart as needed. Also you can see the form in action to view what the end result looks like.

If you have any questions please feel free to ask in the comments section below.

Comments