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

Thank you James for this excellent guide to multi-step form in drupal7.. especially keeping the values intact as we move from one form to the Next(or Back).. looking forward for more on module development articles from you..
Cheers!

Thanks Binu, I'm glad it was helpful. I think I'll do one soon on how to retain values when navigating away from a form and back to it using sessions.

Hi James, Can I kindly request your advise on the below:

How To:
1. Call this multi-step form by a mere click of a link inside another form.
2. On submitting, this multi-step form should close and should take me back to the main form.
3. Also the main form should not refresh (i.e. all its values stay intact up to the time of clicking
its link)

Thanks..

Hi Binu, I'm not sure what you mean? Do you mean being able to navigate between steps through different url's, eg customer-survey/step1, customer-survey/step2 ?

If so you can create a menu callback with the item customer-survey/% where the page argument goes to a function which redirects to different steps of the form depending on the url. Retaining the data is more difficult, you need to save the form_state in a session value which gets loaded at the start of each form load.

I'm going to be writing a post on this soon.

Thank you James for the reply..
Sure i will be waiting for your post on 'retaining values by means of session'
In the meantime, i found Chaos Tools AJAX example(in ctools module) interesting..

Again, thanks a lot.

Hi Binu,

I'm developing exactly what you're waiting from James. Can you comment a little bit which solution you found in Chaos Tools AJAX example(in ctools module). Any link or documentation will be appreciated.

Thanks in advance!
Èric

Hi James,

Thank you so much for you post, it's just what I was looking for.

I'll be waiting your new post to store the values in the session. I'll develop this right now so we can collaborate :)

Cheers!
Èric

Ah you've just reminded me that I was supposed to write a follow up post to this. I'll get on it next week!

Thanks james nice post

I was wondering how we could print out the result of the Survey on the Completion page.

Hi Nick,

On the line which currently says:

if (isset($form_state['complete'])) drupal_goto('complete-page');

You could instead call a function and pass the $form_state['values'] through to it, which would be able to manipulate and display the values which have been passed through. Is this what you had in mind?

James,

This is great, thanks so much!

Question, though: how do I manipulate the results via Views?

Thanks, pb~

Hi Phillip, you wouldn't be able to do this directly. If you wanted to save the information into an entity then it would be possible to access the data in views, but there would be easier ways of capturing data for an entity than building a custom form I think.

Thanks for the tutorial, very easy to follow and adapt.

You should really have this as your default:, otherwise it can cause problems if you have other forms on the same page, like I did ^^

function customer_survey_form($form, &$form_state) {

...

default:
return drupal_form_submit($form, $form_state);
break;
}

return $form;

}

Hope that helps someone else in the future.

Thank you James for the awesome module. Straight-forward, detailed with live demo, appreciating your efforts. I am not a drupal specialist, and I usually disable the navigation block whenever I work with drupal. I did spend a hour, figuring out what I missed and why the navigation does not display, until I switched the theme from Bootstrap to Bartik and it showed up and then I noticed that you use the default navigation block.

Thank you once again.

In addition to my previous comment, the navigation does not work on Bootstrap theme. This is because bootstrap does not render item-list class in the div. There seem to be an issue about this with Tree View module as well here https://www.drupal.org/node/2388415. How would the bootstrap_subtheme_item_list(&variables) look like to get the navigation display in Bootstrap theme.

Thanks.

To get the navigation to work in Bootstrap sub theme:

$form['header'] = array(
'#type' => 'fieldset',
'#title' => t(''),
);

$form['header']['values'] = array(
'#markup' => t($stages_list),
);

How do I use this form?

Is there a way to make this work with uploaded files on a step so an already uploaded file can be remembered in the form state?

Thanks. Awesome. This is really useful for the job I'm doing right now.

Hi,
I want to navigate to specific stage before submitting the form in final stage.
Ex: If there is 6 steps , After completion of 5th step, If I want to directly go to step-1 how I can directly go to that stage.

Any Help.

Hi, just wanted you to know that this article remains helpful 9yrs after you wrote it. :) Slick and easy to understand approach to multi-step forms, thank you! I especially appreciate the tip on keeping the same form_build_id -- I think this would have tripped me up for some time and you saved me that effort.

Thank you,
Tom