Upgrade Your Drupal Skills

We trained 1,000+ Drupal Developers over the last decade.

See Advanced Courses NAH, I know Enough

Building a Multistep Registration Form in Drupal 7

Parent Feed: 

This article provides a step-by-step tutorial for creating a custom, multistep registration form via the Forms API in Drupal 7. For a Drupal 6 guide, I recommend Multistep registration form in Drupal 6.

Drupal 7's updated Form API makes the process of building multistep forms relatively painless. In combination with the excellent Examples for Developers module, it's really just a matter of copy, paste, and tweak.

We're going to be putting a slightly different spin on the standard approach to creating a multistep form.

  • We'll use at least one, pre-existing, system form— the user registration form.
  • We will incrementally trigger #submit handlers after each step, rather than waiting until the end to process all of the form values.
Note:
This code is based of of the example code from in form_example_wizard.inc in the Examples for Developers module. Please use that file as a reference for your own development, as it contains more detailed notes on the functions used here.

The Tut

First things first— create a new, empty, custom module. Since we're going to be overriding the default registration form, you'll want to modify the menu router item that controls the 'user/register' path. We're going to do this with hook_menu_alter().

The 'user/register' path already calls drupal_get_form() to create the registration form, so all we need to do is change which form it will be getting. The name of my form (and form building function) is grasmash_registration_wizard, so that's what we'll use.


Great. We'll get around to actually making that form in just a second. But first, let's set up a a function that will define the other forms that we'll be using. Each step of the multistep form will actually be using an independent form. We'll define those child forms here:

 array(
        'form' => 'user_register_form',
      ),
      2 => array(
        'form' => 'grasmash_registration_group_info',
      ),
    );
}
?>

Note that we're going to be calling the default 'user_register_form' for the first step of the registration process. That will get the important things out of the way, freeing the user to abandon the subsequent steps without screwing everything up.

To create more steps, just add additional rows to that array.

Next, we'll build the parent form grasmash_registration_wizard. This will generate the 'next' and 'previous' buttons, take care of helping us move between steps, and attach the necessary #validate and #submit handlers to each of our child forms. You probably won't need to modify this function much, but you may want to at least change the drupal_set_title() parameters.

 $step)));

  // Call the function named in $form_state['step_information'] to get the
  // form elements to display for this step.
  $form = $form_state['step_information'][$step]['form']($form, $form_state);

  // Show the 'previous' button if appropriate. Note that #submit is set to
  // a special submit handler, and that we use #limit_validation_errors to
  // skip all complaints about validation when using the back button. The
  // values entered will be discarded, but they will not be validated, which
  // would be annoying in a "back" button.
  if ($step > 1) {
    $form['prev'] = array(
      '#type' => 'submit',
      '#value' => t('Previous'),
      '#name' => 'prev',
      '#submit' => array('grasmash_registration_wizard_previous_submit'),
      '#limit_validation_errors' => array(),
    );
  }

  // Show the Next button only if there are more steps defined.
  if ($step < count($form_state['step_information'])) {
    // The Next button should be included on every step
    $form['next'] = array(
      '#type' => 'submit',
      '#value' => t('Next'),
      '#name' => 'next',
      '#submit' => array('grasmash_registration_wizard_next_submit'),
    );
  }
  else {
    // Just in case there are no more steps, we use the default submit handler
    // of the form wizard. When this button is clicked, the
    // grasmash_registration_wizard_submit handler will be called.
    $form['finish'] = array(
      '#type' => 'submit',
      '#value' => t('Finish'),
    );
  }

  $form['next']['#validate'] = array();  
  // Include each validation function defined for the different steps.
  // First, look for functions that match the form_id_validate naming convention.
  if (function_exists($form_state['step_information'][$step]['form'] . '_validate')) {
    $form['next']['#validate'] = array($form_state['step_information'][$step]['form'] . '_validate');
  }
  // Next, merge in any other validate functions defined by child form.
  if (isset($form['#validate'])) {
    $form['next']['#validate'] = array_merge($form['next']['#validate'], $form['#validate']);
    unset($form['#validate']);
  }


  // Let's do the same thing for #submit handlers.
  // First, look for functions that match the form_id_submit naming convention.
  if (function_exists($form_state['step_information'][$step]['form'] . '_submit')) {
    $form['next']['#submit'] = array_merge($form_state['step_information'][$step]['form'] . '_submit', $form['next']['#submit']);
  }
  // Next, merge in any other submit functions defined by child form.
  if (isset($form['#submit'])) {
    // It's important to merge in the form-specific handlers first, before 
    // grasmash_registration_wizard_next_submit clears $form_state['values].
    $form['next']['#submit'] = array_merge($form['#submit'], $form['next']['#submit']);
    unset($form['#submit']);
  }

  return $form;
}
?>

Next, we're going to lift the 'next' and 'previous' submit handler functions directly from the example module. There's no need to modify these (unless you really want to).

 1) {
    $current_step--;
    $form_state['values'] = $form_state['step_information'][$current_step]['stored_values'];
  }
  $form_state['rebuild'] = TRUE;
}

/**
 * Submit handler for the 'next' button.
 * - Saves away $form_state['values']
 * - Increments the step count.
 * - Replace $form_state['values'] from the last time we were at this page
 *   or with array() if we haven't been here before.
 * - Force form rebuild.
 *
 * You are not required to change this function.
 *
 * @param $form
 * @param $form_state
 *
 * @ingroup form_example
 */
function form_example_wizard_next_submit($form, &$form_state) {
  $current_step = &$form_state['step'];
  $form_state['step_information'][$current_step]['stored_values'] = $form_state['values'];

  if ($current_step < count($form_state['step_information'])) {
    $current_step++;
    if (!empty($form_state['step_information'][$current_step]['stored_values'])) {
      $form_state['values'] = $form_state['step_information'][$current_step]['stored_values'];
    }
    else {
      $form_state['values'] = array();
    }
    $form_state['rebuild'] = TRUE;  // Force rebuild with next step.
    return;
  }
}
?>

Now it's time to build the child forms. In the case of 'user_register_form', the form already exists. Since we aren't building it, we'll have to use hook_form_alter() to make the necessary modifications. We can target the user_register_form in specific by requiring that $form_state['step'] == 1.


That takes care of step one. Now I'll build the form for step 2. This is where most of your modifications will come in to play.

 'fieldset',
      '#title' => t('Create a new community.'),
    );

    // Allow users to create a new organic group upon registration.
    $form['create-group']['group-name'] = array(
      '#type' => 'textfield',
      '#title' => t('Community name'),
      '#description' => t('Please enter the name of the group that you would like to create.'),
      '#required' => TRUE,
      '#weight' => -40,
    );

    $form['#validate'][] = 'grasmash_registration_validate_og';
    $form['#submit'][] = 'grasmash_registration_new_og';
    
    return $form;
}
?>

I'm not going to be including the step 2 submit or validate handlers in this tutorial, since they are specific to my current project. The important point is that for each step, you can alter or build any form you'd like.

Lastly, we'll define the final #submit function. This will be run when the user completes the last step of the form. You'll probably want to display a message and issue a redirect.

 $value) {
    // Remove FAPI fields included in the values (form_token, form_id and form_build_id
    // This is not required, you may access the values using $value['stored_values']
    // but I'm removing them to make a more clear representation of the collected
    // information as the complete array will be passed through drupal_set_message().
    unset($value['stored_values']['form_id']);
    unset($value['stored_values']['form_build_id']);
    unset($value['stored_values']['form_token']);

    // Now show all the values.
    drupal_set_message(t('Step @num collected the following values: 
@result
', array('@num' => $index, '@result' => print_r($value['stored_values'], TRUE)))); } // Redirect the new user to their user page. $user = user_load($form_state['uid']); drupal_goto('user/' . $user->uid); } ?>

Not too bad! This structure will allow you to easily add new steps using existing or custom forms, and to easily add #submit and #validate handlers at any step in the process.

Closing Thoughts

The method above works well, but I would be remiss if I didn't mention Ctools built-in multi-step form wizard. In addition to providing a framework for multi-step forms, Ctools also offers an excellent method for caching data in between steps. I may write an article on implementing this API as well. For now, take a look at the help document packaged with ctools in ctools/help/wizard.html. You may also want to take a look at this tutorial on using the ctools multi-step form wizard.

Update

I've written a follow up to this article, explaining Building a Multistep Registration Form in Drupal 7 using Ctools.

Good luck!

Author: 
Original Post: 

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web