Building a Multistep Registration Form in Drupal 7

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.

<?php
/**
 * Implements hook_menu_alter().
 */
function grasmash_registration_menu_alter(&$items) {
  $items['user/register']['page arguments'] = array('grasmash_registration_wizard');
  return $items;
}
?>

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:

<?php
function _grasmash_registration_steps() {
  return array(
      1 => 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.

<?php
function grasmash_registration_wizard($form, &$form_state) {

  // Initialize a description of the steps for the wizard.
  if (empty($form_state['step'])) {
    $form_state['step'] = 1;

    // This array contains the function to be called at each step to get the
    // relevant form elements. It will also store state information for each
    // step.
    $form_state['step_information'] = _grasmash_registration_steps();
  }
  $step = &$form_state['step'];
  drupal_set_title(t('Create Account: Step @step', array('@step' => $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).

<?php
/**
 * Submit handler for the "previous" button.
 * - Stores away $form_state['values']
 * - Decrements the step counter
 * - Replaces $form_state['values'] with the values from the previous state.
 * - Forces form rebuild.
 *
 * You are not required to change this function.
 *
 * @ingroup form_example
 */
function form_example_wizard_previous_submit($form, &$form_state) {
  $current_step = &$form_state['step'];
  $form_state['step_information'][$current_step]['stored_values'] = $form_state['values'];
  if ($current_step > 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.

<?php
function grasmash_registration_form_alter(&$form, &$form_state, $form_id) {
  if ($form_id == 'grasmash_registration_wizard' && $form_state['step'] == 1) {
    // Clean up the form a bit by removing 'create new account' submit button
    // and moving 'next' button to bottom of form.
    unset($form['actions']);
    $form['next']['#weight'] = 100;
  }
}
?>

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.

<?php
/**
 * Build form elements for step 2.
 */
function grasmash_registration_group_info(&$form, &$form_state)  {

    $form['create-group'] = array( 
      '#type' => '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.

<?php
// And now comes the magic of the wizard, the function that should handle all the
// inputs from the user on each different step.
/**
 * Wizard form submit handler.
 * - Saves away $form_state['values']
 * - Process all the form values.
 *
 * This demonstration handler just do a drupal_set_message() with the information
 * collected on each different step of the wizard.
 *
 * @param $form
 * @param $form_state
 *
 * @ingroup form_example
 */
function form_example_wizard_submit($form, &$form_state) {
  $current_step = &$form_state['step'];
  $form_state['step_information'][$current_step]['stored_values'] = $form_state['values'];

  // In this case we've completed the final page of the wizard, so process the
  // submitted information.
  drupal_set_message(t('This information was collected by this wizard:'));
  foreach ($form_state['step_information'] as $index => $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: <pre>@result</pre>', 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!

In reply to by grasmash

It was great to land on this blog post and to read such amazing stuff. Your blog is full of authentic and highly-researched information that is worth reading. I will surely recommend your blog to my fellows!
http://www.convertvideos.org/

What advantages does this have over using the CTools Form Wizard and Object Cache for multistep?

In reply to by Kevin

It doesn't offer any advantages, apart from the fact that you won't need to rely on ctools— you can just use the core Form API.

I've begun writing a separate tutorial for the Ctools form wizard, which is beginning to seem like a better alternative to me.

First off thanks for this. It is very helpful. The only issue that I'm having is that the validate hooks are not getting called. If I dpr($form['next']['#validate']), I get


Array
(
[0] => my_module_confirm_account_validate
)

and I also have


function marin_custom_confirm_account_validate($form, &$form_state) {
print "Validate Hook";
die;
}

But after clicking next, nothing is printed and the execution isn't stopped. Any ideas?

In reply to by A. J.

I know it's been a while but validation worked for me using the following:

// Replace form_id with the name of the form listed in steps()
function form_id_validate($form, &$form_state) {
if (!is_numeric($form_state['values']['zip_code'])) {
form_set_error('zip_code', t('This field should only consist of numbers.'));
}
}

This worked fine for me. The validate functions don't need to be listed in the steps function. They are called in the wizard function.

In reply to by KenJ

That's right. Drupal will automatically detect validations functioned named using the pattern [form_id]_validate($form, &$form_state){}.

You can also add your own validation functions by adding strings to the $form['#validate'] array. E.g.,

Thank you for this tutorial, it was good help to me!

1) Here is one modification I had to do in my case, if it can help people having same issues:

in the profile2_regpath_registration_wizard($form, &$form_state) function, i replaced
$form = drupal_get_form($form_state['step_information'][$step]['form']);
by

$form_id = $form_state['step_information'][$step]['form'];
if(!isset($form_state)){
$form_state = array();
}
$form = drupal_retrieve_form($form_id, $form_state);
drupal_prepare_form($form_id, $form, $form_state);

with doing so, all the hook_form_alter functions defined for the user_register_form will be called. useful if you installed modules to tune your user_register_form, like the logintoboggan module.

Then, to be consistent with drupal specification (and trigger no error when calling drupal_prepare_form) the $form parameter in the grasmash_registration_group_info(&$form, &$form_state) function should be passed by value --> signature should be grasmash_registration_group_info($form, &$form_state)

2) Some minor mistake i also noticed: to be consistent with the beginning of the code, the names of the function starting with 'form_example_wizard_' should actually start with 'grasmash_registration_wizard_'

Thank you again.

Could you please tell me where I have to put in all the code above??
just grasmash_registration_wizard.module or what? :S

Need your help!! :)

Hi, Thanks for the tutorial, very much needed.
only problem is there is no information on how and where to use the code,
I've put it in an empty module and enabled it but how to invoke the form?
please help...

In reply to by Renata

Yes, you should put the code into an empty module and enable it. The purpose of using hook_menu_alter() is to modify the router item for 'user/register' so that you can invoke the form at that path. Be sure to clear your caches after making any modifications to the menu system.

should i put these all code into .module file?

Created form by that instruction. Works very strange - after second step always redirecting in on the pages like %mydrupalsite%/user/645/edit (645 - new user number). When I manually set the start on the second step - everything works fine. (I mean changing variable in your third codeblock line 5). After debugging I understood that something replacing next button, because submitter doesn't works. I am in panic! How can I fix it? could you help me?

Thanks for the great tutorial! It's lacking one piece of critical code: passing the stored values to create the actual user. I know that the original code is specific to your project, but can you give us an example? Thanks!

Is there a way to add profile2 forms into this mix ...
What I need is perhaps step 2 to be personal profile2 info and a conditional set up if they need a second profile. If that conditional is met then they go to step 3 and fill out a second profile2 form.

Hello Thanks for this multi step user registration form.
but i have created the empty module and copied the above code into module and enabled it but my user registration form is not changing. It is same as before it was. can any one help me how to use this code.

Hi,
Thank you for your excellent post!
My problem is that my custom submit handler at "grasmash_registration_group_info" is not fired
.................
$form['#submit'][] = 'grasmash_registration_,my_custom_submit;
...............
function grasmash_registration_,my_custom_submit(&$form, &$form_state) {
drupal_set_message('Start Submit my_custom_submit');
}

And also produce the following warnings:

Notice: Undefined index: #submit in grasmash_registration_wizard()
and
Argument #2 is not an array in grasmash_registration_wizard()

both at line:
$form['next']['#submit'] = array_merge($form['#submit'], $form['next']['#submit']);

Thanks in advance for any help.

Hello!!

I get the same error that kyriakoy get.

Notice: Undefined index: #submit in grasmash_registration_wizard()
and
Argument #2 is not an array in grasmash_registration_wizard()

both at line:
$form['next']['#submit'] = array_merge($form['#submit'], $form['next']['#submit']);

Has someone solved this error?

Thanks in advance ;-)

In reply to by harden13

I encountered this same issue and I was able to fix it by changing that line to:

$form['next']['#submit'] = array_merge(array($form['#submit']), array($form['next']['#submit']));

This is because the array_merge function requires arrays for its arguments. This is actually a problem for all the instances of array_merge() in that section of code.

Hope this helps!

Hi madmatter23,

Thanks for the code above. I need some help. Please help me to get a search form sort of functionality. I want a form with one text field and one submit button. When I'll click on the submit for the data entered in the text field, which will be the ID of some node, then it should return the id of the node with the link to same node. And the data should be displayed in the same page showing the search form with values entered. Please reply it's urgent. And please provide the procedure to implement it, if possible.

Any help would be appreciated.

Thanks,

on the official web portal built a huge selection of the latest news about <a href=http://chromebrowser.ru/>браузер гугл хром</a>

sdfdf

Although there hasn't been any activity since years over here, I hope there is still somebody who can help me with the problem I'm having:
I want to move all submit handlers to the last submit. I have 4 steps, where the user register form is in the second. But I want the user to be created after step 4.
I can't get this to work. Any tips?

Add new comment

Plain text

  • No HTML tags allowed.
  • Lines and paragraphs break automatically.