I found some code on the web to implement multi-step forms in Drupal 7, and currently dissecting it and trying to understand it. (My PHP understanding is decent, but not "great.") I've got most of it worked out and understood, and now I'm flipping the code to put into a multi-step user registration form. So I print out the $form array on the user registration form, and here's a snippet of what I get (edited down for simplicity).


Array
(
    [account] => Array
        (
            [name] => Array
                (
                    [#type] => textfield
                    [#title] => Username
                    [#maxlength] => 60
                    [#description] => Spaces are allowed; punctuation is not allowed except for periods....
                )

            [mail] => Array
                (
                    [#type] => textfield
                    [#title] => E-mail address
                    [#description] => A valid e-mail address. All e-mails from the system...
                )
        )
)

Now here's a snippet of my code for my custom module...

  static $steps = array(
    'account'              => -1,
  );
  return $steps;

In a nutshell, this code targets and manipulates everything in the [account] array, and it works.

However, what I need to do is actually target the 'account => mail' and 'account => name' fields/arrays specifically. How would I do that in the above PHP code? I tried variations like 'account->name', 'account => name' and even 'account[name]' but nothing works. The 'account' in the code above is listed in a string so I can understand why those previous attempts didn't work, but removing the quotes invalidates the code.

Comments

WebMaster’s picture

Hmmm, something I overlooked, the line that $steps (as $key in the loop) is called....

  foreach ($steps as $key => $step_current) {
    if ($step_current == $step) { 
      if (isset($form['actions']['next'])) {
        $form['actions']['next']['#limit_validation_errors'][] = array($key);
      }
    }
    else {
      $form[$key]['#access'] = FALSE; // Sets access permissions on fields
    }
  }

Particularly it's the "else" statement that I'm trying to make work. Now I understand why 'account' has to be a single variable and why manipulating it didn't work. There would be no problem if 'name' and 'mail' was on the same level of the array as 'account', but it's one level down.

I changed the $form[$key]['#access'] line to $form['account'][$key]['#access'] and it worked, but that will only work on arrays on that particular level. Acceptable for now I suppose, but problematic if I need to target something a level up on the while loop. Trying to figure out how to revise this code...

jason_gates’s picture

Hi,
Are you saying you want to take the existing user registration form and break into steps? Or, are you adding fields to the existing registration form and then breaking that extended form in to "multi-steps"?

Are you simply trying to figure our how to implement a multi-step form with Drupal 7? Or do you need to customize a user registration form specifically?

The Drupal form api (fapi) docs are located here:
http://drupal.org/node/751826
http://api.drupal.org/api/drupal/developer--topics--forms_api_reference....
http://drupal.org/project/examples
http://api.drupal.org/api/drupal/includes--form.inc/group/form_api/7

Great, looks like you just added some more source code from whatever tutorial you are using. You need to post a lot more for anyone to help you :)

In general, you implement hook_form_alter() http://api.drupal.org/api/drupal/modules--system--system.api.php/functio... to customize an existing form. In addition, you need to become familiar with the different form handling phases (definition, validation, submission). You'll need to implement a custom submit handler to control the various steps. You will also need to customize the form definition so that it stores the various "step" states.

My recommendation is to get into the habit of reviewing Drupal's source code and get your self a source code debugger (like Netbeans with xdebug http://netbeans.org/). Some of the form you are trying to customize resides in the user.module listing, user_account_form() method. I'm not say change the source, but look through it to see how the form is being constructed and built. No need to guess, Drupal is open source, That means you can look right at the source code :)

Feel free to peruse my blog http://public-action.org/content/drupal-7-form-api-drupal-7-multi-step-f.... It's a post showing you how to implement a multi-step form using Drupal 7's form api. It might help. The module examples located here http://drupal.org/project/examples are always the best place to start.

Hope that helps.

WebMaster’s picture

Hi Jason,

Thanks, I'll check some of that out, though some I know already. I also came across that tutorial on Google today, but as I wasn't building a form from scratch, I didn't look too much into it yet and wasn't sure it applied to my situation. (In addition, I had already build some of my following code from a while back prior to that tutorial existing.)

Pertaining to your first question, I guess I'm saying a little of both. Right now I'm working on making the user registration form multi-step as is: just name and email fields. Later on, I can foresee adding additional fields to the user registration form and/or a terms of use, so that's where my concern about targeting the array level comes in.

I am adapting the code based on this tutorial and have already successfully implemented it into a node add/edit form. I've taken that code and re-purposed it with different hooks into a multi-step registration form, and I actually have most of that working too. The fields validate / error if not filled in, and you can go forward and back, yay. The only issues right now is that the values are not saved whenever you progress or go back in the form, plus the "create new account" button still shows up on the first step, but admittedly I'm still working on the code at this moment and have overlooked something. Below is current of this moment:


define('MULTIFORM_USER_REGISTRATION_LAST_STEP', 2);
function multiform_form_user_register_form_alter(&$form, &$form_state, $form_id) {

  $step = isset($form_state['storage']['step']) ? $form_state['storage']['step'] : 1;
  if ($step > 1) {
    $form['actions']['previous'] = array(
      '#type'   => 'submit',
      '#value'  => t('Previous'),
      '#submit' => array('multiform_node_form_previous', 'user_account_form'),
      // -1 so that it appears before the "Next" button added later.
      '#weight' => -1,
      '#limit_validation_errors' => array(),
    );
  } 
  if ($step < MULTIFORM_USER_REGISTRATION_LAST_STEP) {
    $form['actions']['next'] = array(
      '#type'   => 'submit',
      '#value'  => t('Next'),
      '#submit' => array('multiform_node_form_next', 'user_account_form'), 
    );
  }
  $steps = multiform_node_form_by_step();
  foreach ($steps as $key => $step_current) {
    if ($step_current == $step) {
      if (isset($form['actions']['next'])) {
        $form['actions']['next']['#limit_validation_errors'][] = array($key);
      }
    }
    else {
      $form['account'][$key]['#access'] = FALSE;
    }
  }
}
function multiform_node_form_by_step() {

  static $steps = array(
    'name' => 1,
    'mail' => 2,
  );
  return $steps;
}

// Triggers the next button function.
function multiform_node_form_next($form, &$form_state) {
  if (!isset($form_state['storage']['step'])) { 
    $form_state['storage']['step'] = 1; 
  }
  if ($form_state['storage']['step'] == MULTIFORM_USER_REGISTRATION_LAST_STEP) { 
    $form_state['rebuild'] = FALSE; 
  }
  else {
    $form_state['rebuild'] = TRUE;
    $form_state['storage']['step']++; 
  }
}

// Triggers the previous button function
function multiform_node_form_previous($form, &$form_state) {
  $form_state['rebuild'] = TRUE; 
  $form_state['storage']['step']--; 
}

// Unsure what this does?
function multiform_field_attach_validate($obj_type, $object, &$errors) {
  if (arg(0) == 'node' && (arg(1) == 'add' || arg(2) == 'edit')) {
    // foo and bar fields are handled elsewhere so nuke their errors.
    unset($errors['foo'], $errors['bar']);
  }
}

jason_gates’s picture

Hi,
To remove the "create new account" button, looks like you should unset($form['actions']['submit']);. I'm looking a the source that creates the button (user.module, user_register_form() ). That is where I got the $form element definition from.

Next, change the following:

'#submit' => array('multiform_node_form_next', 'user_account_form'), 

to

'#submit' => array('multiform_node_form_next', 'user_register_submit'), 

What the code is doing is, defining a list of functions that are called when the button is clicked (submitted). You want to call the user_register_submit() method (also in user.module),

FYI -- Really nice job for someone new to the Drupal form api !! :) Really close. Sorry, I haven't tested the code. I looked at your code, checked the source, etc. See if that helps and I'll check back here. Again. really nice work!! :)

Ooops! Almost forgot. You should add a switch statement to your hook_form_alter implementation. As written, your module will attempt to modify every form. You want something like switch($form_id) { case 'user_register_form' : ...... break; }

WebMaster’s picture

The unset suggestion for the button worked, thanks.

I was looking through the D7 API for a user registration function (and I got lost looking through the user module), I used user_account_form to see what would happen, but yes, nothing. As for replacing it with 'user_register_submit,' what happens when I do is that the user form automatically submits whenever I click the next button. So I took that off and just left the 'multiform_node_form_next' function.

The big issue with the form currently is not remembering values when you come back to them. I noticed some multi-step forms pass values to $form_state, but what is weird is in the example tutorial I'm using for the multi-step node form, I don't see anything being passed to $form_state but the step number, and all fields remain filled in when you go forward and back and submit. Wonder why it works for that but not this.

WebMaster’s picture

I think I got it worked out now as far as the disappearing fields goes, I did some tweaking based on the tutorial Jason posted along with the Examples module (very, very cool, BTW) and came out with the following (note, haven't implemented the "case" yet as suggested)....

define('MULTIFORM_USER_REGISTRATION_LAST_STEP', 2);
function multiform_form_user_register_form_alter(&$form, &$form_state, $form_id) {

  $step = isset($form_state['storage']['step']) ? $form_state['storage']['step'] : 1;

  $form['account']['name']['#default_value'] = !empty($form_state['storage']['name']) ? $form_state['storage']['name'] : '';
  $form['account']['mail']['#default_value'] = !empty($form_state['storage']['mail']) ? $form_state['storage']['mail'] : '';

  if ($step > 1) { // Build Previous button only if past the first step
    $form['actions']['previous'] = array(
      '#type'   => 'submit',
      '#value'  => t('Previous'),
      '#submit' => array('multiform_node_form_previous', 'user_account_form'),
      // -1 so that it appears before the "Next" button added later.
      '#weight' => -1,
      '#limit_validation_errors' => array(),
    );
  } 
  if ($step < MULTIFORM_USER_REGISTRATION_LAST_STEP) {
    $form['actions']['next'] = array(
      '#type'   => 'submit',
      '#value'  => t('Next'),
      '#submit' => array('multiform_node_form_next'), 
    );
    unset($form['actions']['submit']);
  }
  $steps = multiform_node_form_by_step();
  foreach ($steps as $key => $step_current) {
    if ($step_current == $step) {
      if (isset($form['actions']['next'])) {
        $form['actions']['next']['#limit_validation_errors'][] = array($key);
      }
    }
    else {
      $form['account'][$key]['#access'] = FALSE;
    }
  }
}

function multiform_node_form_by_step() {
  static $steps = array(
    'name' => 1,
    'mail' => 2,
  );
  return $steps;
}

// Triggers the next button function.
function multiform_node_form_next($form, &$form_state) {

  $form_state['storage']['name'] = !empty($form_state['values']['name']) ? $form_state['values']['name'] : '';
  $form_state['storage']['mail'] = !empty($form_state['values']['mail']) ? $form_state['values']['mail'] : '';

  if (!isset($form_state['storage']['step'])) { 
    $form_state['storage']['step'] = 1; 
  }
  if ($form_state['storage']['step'] == MULTIFORM_USER_REGISTRATION_LAST_STEP) { 
    $form_state['rebuild'] = FALSE; 
  }
  else {
    $form_state['rebuild'] = TRUE;
    $form_state['storage']['step']++;  
  }
  
}

// Triggers the previous button function
function multiform_node_form_previous($form, &$form_state) {

  $form_state['rebuild'] = TRUE; 
  $form_state['storage']['step']--;
}

So basically it stores the variables with the $form_state. Again, curious why the other example did not need it, but can't knock it if it works.

But I still have one lingering issue, and this goes back to my original question. Via Drupal 7 fields, I added a "Full Name" field (field_profile_name) to the user account and displayed it on user registration. As I thought it would, the $form array comes out like this (I'm not posting the entire thing here, shortening it for simplicity's sake)...

Array
(
    [account] => Array
        (
            [name] => Array
                (
                )

            [mail] => Array
                (
                )

    [field_profile_name] => Array
        (
        )

So basically, the field_profile_name lives one level up from the name and mail arrays, where the regular user registration form items lives. As I pointed out in my second post, in the form_alter hook function I had to use "$form['account'][$key]['#access']" as opposed to "$form[$key]['#access']" in order to not display the username or email fields on the steps they don't belong on.

So is there a better way of working that portion of the code to traverse the array based on level?

jason_gates’s picture

Hi,
Really great job!! :)

You just solved many different issues all in one post:

  • How to alter an existing form - Drupal 7
  • How to alter a one step form to a multi-step form Drupal 7
  • How to alter the user registration form Drupal 7
  • How to implement multi-step forms in Drupal 7

Please consider other folks in the community who will be searching this forum when facing the same challenge. Those folks will be looking at the subject line of your original post. Currently, your subject line does not describe the actual issue solved. Therefore, please change the subject line and please prefix it with [solved] (per the forum guideline http://drupal.org/forum-posting ) :)

Sorry to harp on the point of changing the subject line, but again, you've demonstrated how to solve an important challenge. Please contribute back to the community by making you post available via a search. How is it available via a search? Answer: Change the subject line so that it reflects the main issue (e.g. Altering User Registration Form to Multi-Step Drupal 7, or something better :) ).

I'll be happy to answer you second subject briefly, However, please consider creating a separate thread if you indeed want to engage in a lengthy discussion of "How do inspect Drupal's Data Structures". That subject to me, is a separate thread. Again, why a separate thread? Well we want other folks to be able to find the solution.

Now briefly -- Data Structures:

Drupal 7 expands functionality. For example, a programmer can now dynamically add all sorts of attributes dynamically to "input forms", "page reports", the database, widgets, content types, fields, field attributes, etc etc etc etc.

As Drupal processes all of these dynamic attributes, it builds up complex data structures. Your question is, "How to I inspect a dynamic complex data structure?". The answer is:

  • Always develop on a local installation. Never try to analyze a remote installation. That means install Drupal on your local computer (e.g. laptop). A local installation allows you to inspect the system logs where many PHP runtime errors are printed. Most hosting services will not give you access to those logs. A local installation also allows you to run Drupal through a real time source tracer and debugger.
  • Related to a debugger is an IDE (e.g Netbeans) which includes a slew of tools to help you view complex data structures, display syntax errors, generates code segments. You need a local installation of Drupal to run it through a IDE/Debug stack.
  • Drupal uses both objects and arrays. Objects and arrays can be nested inside of one another. Thus, a programmer needs to be able to inspect not only the real time data structures, but the source code that is generating it. How do you do that, with a debugger.
  • Never rely on static data structure dumps (e.g. print statements). It's like telling someone to dig the foundation of their house with a tea spoon. Print statements are so archaic in a dynamic environment, they become counter productive.

Again hope that helps. Please change the subject line of you post so that other folks in the community can use it.

Also, the tutorial you referenced uses a static variable to maintain state. I agree, if it works we have plenty of better things to do than critique working code. I personally don't recommend using that portion of the code. Using the $form_state['storage'] data structure is a standard Drupal solution, thus preferable.

WebMaster’s picture

So it was that static line of the example code that was saving the variables, that explains a lot. So I guess I went the right route after all ;)

Thanks for all your help. While I still need answers to my original question, I will repost as a new topic, though would like to see if I can get it working first before I post a new one.

ermanpoe’s picture

Thanks for your work.
This is very helpfull. I'm new on drupal and i want to do a multistep registration with profile module. But a fieldset has been generated and stay on my step 1. How can i manage this ?

thanks
ps :(english is not my first language, so, sorry for the mistakes)

jtamasaf’s picture

I am not sure where does this code go?
Is it placed in user.module file of the user module? or is it neccesary to create our module?

Which lines must be changed to call this multiform_form_user_register_form_alter fucntion? and where?

Thank you

boby_ui’s picture

Hi,

the code works great! I am trying to pull custom profile2 that I created for user registration and I am struggling on using it to step2? is there any clue how to implement it?

thank you

zeta1600’s picture

Boy, I would love to be able to do this, but... It all sounds foreign to me as a designer. Would it be possible to have a step-by-step or maybe a module out of this?

mutuku’s picture

I am trying to use this code, The username field is hidden in step 1, but shows up empty in step 2. How do i hide it in step 2?

racksjackson’s picture

You can change code of step 2 according to your program where function call define('MULTIFORM_USER_REGISTRATION_LAST_STEP', 2);

ardnet’s picture

I'm subscribing this thread for my future references.
I also need to build multi step registration form in Drupal 7.
Thanks a lot for the light guys :)

Cheers

CRR-1’s picture

Drupal password field is needed to be hidden. But then i can't return it to work and to save data that user inputs in step 1.
Any ideas?

fehin’s picture

subscribing.

benjf’s picture

this is a superb reference, thank you!

my only problem is that I cannot seem to get the password field to save properly. I have most fields on step 1, including password. After completing step 2 and clicking the 'create account' button, I get an error about a missing password field.

any tips on this one?

thanks again,
-Benj

da_solver’s picture

Hi,
I would recommend posting the relevant portions of your code. It's highly unlikely someone can offer suggestions if they haven't reviewed what you coded :)

Also, note the strong recommendation for developing locally with a source code debugger. Are developing on a local installation with a debugger, as recommended?

Hope that helps :)
Good Luck

chirhotec’s picture

I'm having the same problem with not being able to save due to Password validation failing. Here's how to reproduce the problem:
(1) Go to Admin > Configuration > Account Settings, and disable "Require e-mail verification when a visitor creates an account", so that the registering user can set their password on the form
(2) Modify the above "multiform_node_form_by_step" function to be:

function multiform_node_form_by_step() {
  static $steps = array(
    'name' => 1,
    'pass' => 1,
    'mail' => 2,
  );
  return $steps;
}

(3) Modify the beginning of the "multiform_node_form_previous" to be:

function multiform_node_form_next($form, &$form_state) {

  $form_state['storage']['name'] = !empty($form_state['values']['name']) ? $form_state['values']['name'] : '';
  $form_state['storage']['pass'] = !empty($form_state['values']['pass']) ? $form_state['values']['pass'] : '';
  $form_state['storage']['mail'] = !empty($form_state['values']['mail']) ? $form_state['values']['mail'] : '';

  // Continue with original listing....

The end result is that after you submit the form on page 2, there is a validation error message that says the "Password field is required." I've been trying to step through the code, but without much previous drupal programming experience, I haven't made too much progress. All I can tell is that when the validation is called on the final submission, the name someone makes it way back into the $form_state['values']['name'] field, but the password does get stored back into the $form_state['values']['pass'] field (the password is however still stored in $form_state['storage']['pass'].

Any thoughts on how to fix this problem?

chirhotec’s picture

After digging through the code, I think there may be several different approaches (only one of which I've tested so far). I'm not sure which would be the "correct" way of doing it- they all kind of seem like hacks to me, but thats probably because I'm so new to Form API and the FAPI workflow

Approach #1

The first way is most in line with how the original code was set up. That is, during the "Next" submit handler, the values are pushed into $state_form["storage"], and when the form is subsequently created, those values are pushed into the form elements as their default values.

However, password_confirm elements don't have a #default_element attribute, so this wont work:

function d2d_registration_form_alter(&$form, &$form_state, $form_id) {
  ...
  $form['account']['name']['#default_value'] = !empty($form_state['storage']['name']) ? $form_state['storage']['name'] : '';

  // causes everything to break:
  $form['account']['pass']['#default_value'] = !empty($form_state['storage']['pass']) ? $form_state['storage']['pass'] : '';  

  $form['account']['mail']['#default_value'] = !empty($form_state['storage']['mail']) ? $form_state['storage']['mail'] : '';
  ...

If you look at form.inc's form_process_password_confirm, it actually populated the field not based on a #default_value, but based on a #value attribute not listed in FAPI. And of course, that's split up for the two text fields ($element['#value']['pass1'] and $element['#value']['pass2']). Keeping it in line with the original solution might production something like this (which also doesn't work quite yet):

function d2d_registration_form_alter(&$form, &$form_state, $form_id) {
  ...
  $form['account']['name']['#default_value'] = !empty($form_state['storage']['name']) ? $form_state['storage']['name'] : '';

  // doesn't work: overrides user submitted data with empty data
  $form['account']['pass']['#value']['pass1'] = !empty($form_state['storage']['pass']) ? $form_state['storage']['pass'] : '';
  $form['account']['pass']['#value']['pass2'] = !empty($form_state['storage']['pass']) ? $form_state['storage']['pass'] : '';

  $form['account']['mail']['#default_value'] = !empty($form_state['storage']['mail']) ? $form_state['storage']['mail'] : '';
  ...

The problem with this code is that when the form is submitted (via Next button) its rebuilt before the Next button's submission handler is called. Thus, the password field is rebuilt with blank values b/c the submission handler hasn't put the values in storage yet. And these blank values override whatever the user just submitted. So that page actually fails to validate even though the user just entered in the password.

So the solution is to only push the stored values to the form element, if the stored values actually exist:

function d2d_registration_form_alter(&$form, &$form_state, $form_id) {
  ...
  $form['account']['name']['#default_value'] = !empty($form_state['storage']['name']) ? $form_state['storage']['name'] : '';

  if(!empty($form_state['storage']['pass'])) {
    $form['account']['pass']['#value']['pass1'] = $form_state['storage']['pass'];
    $form['account']['pass']['#value']['pass2'] = $form_state['storage']['pass'];
  }

  $form['account']['mail']['#default_value'] = !empty($form_state['storage']['mail']) ? $form_state['storage']['mail'] : '';
  ...

This will work (when used with the original code snippets I listed in the previous comment), but it feels like a bit of a hack to me, given what the documentation says about #value and password_confirm.

Other Approaches

The other approaches I think might work would be to use a #process callback for the password_confirm element, inserted before form.inc's form_process_password_confirm. Or to use an #element_validate callback, inserted before form.inc's password_confirm_validate function. (I tried just using a general form validator, but it was being called after the individual element's validators). The only other approach I can think of would be to use the form_type_hook_value to insert all the stored values back into the form, but I'm not really sure how to set that up.

navinkmrsingh’s picture

This is what I needed. Thanks everyone......... :)

jenlampton’s picture

Hi guys, the following is some code I got working for one of my projects. It needs to be added into your own custom module, but should work almost as-is. Just change everywhere you see "mymodule" in the code below to the name of your module and this should do the trick.

/**
 * @file 
 * Customizations to the Drupal registration system.
 */

/**
 * Implements hook_form_FORM_ID_alter().
 */
function mymodule_form_user_register_form_alter(&$form, &$form_state) {
  // Make this a multi-step form.
  if (!empty($form_state['step']) && $form_state['step'] == 2) {
    mymodule_register_alter_page_two($form, $form_state);
  }
  else {
    // Otherwise we build page 1.
    mymodule_register_alter_page_one($form, $form_state);
  }
}

/**
 * Form alter - Step 1 of user registration.
 */
function mymodule_register_alter_page_one(&$form, &$form_state) {
  // Set the step.
  $form_state['step'] = 1;

  // Add text for step 1.
  $form['step'] = array(
    '#markup' => '<h3>' . t('Step 1 of 2: Account information') . '</h3>',
    '#weight' => -10,
  );

  // Hide the information for step 2.
  $form['field_stuff']['#access'] = FALSE;
  $form['field_abc']['#access'] = FALSE;
  $form['field_whatevs']['#access'] = FALSE;

  // Set default values (so they work with back button).
  $form['account']['name']['#default_value'] = !empty($form_state['values']['name']) ? $form_state['values']['name'] : '';
  $form['account']['mail']['#default_value'] = !empty($form_state['values']['mail']) ? $form_state['values']['mail'] : '';

  // Add a next button.
  $form['actions']['next'] = array(
    '#type' => 'submit',
    '#value' => 'Next >>',
    '#submit' => array('mymodule_register_next'),
  );

  // Remove the 'Create new account' button from step 1.
  unset($form['actions']['submit']);
}


/**
 * Form alter - Step 2 of user registration.
 */
function mymodule_register_alter_page_two(&$form, &$form_state) {
  // Add text for step 2.
  $form['step'] = array(
    '#markup' => '<h3>' . t('Step 2 of 2: Establishment information') . '</h3>',
    '#weight' => -10,
  );

  // Hide the fields completed on step 1.
  $form['account']['name']['#access'] = FALSE;
  $form['account']['mail']['#access'] = FALSE;

  // Set default values (so they validate).
  $form['account']['name']['#default_value'] = !empty($form_state['values']['name']) ? $form_state['values']['name'] : '';
  $form['account']['mail']['#default_value'] = !empty($form_state['values']['mail']) ? $form_state['values']['mail'] : '';

  // Add a back button.
  $form['actions']['back'] = array(
    '#type' => 'submit',
    '#value' => t('<< Back'),
    '#submit' => array('mymodule_register_back'),
    '#limit_validation_errors' => array(),
  );

   // Adjust the submit button to come last.
  $form['actions']['submit']['#weight'] = 100;
  // Add our own validation handler.
  $form['actions']['submit']['#validate'] = array('mymodule_register_validate');

  // Add an additional submit handler to the whole form.
  $form['#submit'][] = 'mymodule_register_submit';
}

/**
 * Validate handler.
 */
function mymodule_register_validate(&$form, &$form_state) {
  ...
}

/**
 * Submit handler for user registration form.
 *
 * Namespace collision demands underscore.
 */
function mymodule_register_submit(&$form, &$form_state) {
  ...
}

/**
 * Submit handler for next button.
 *
 * Capture the values from page one and store them away so they can be used
 * at final submit time.
 */
function mymodule_register_next($form, &$form_state) {
  // Save the page 1 values.
  $form_state['page_values'][1] = $form_state['values'];

  // Load the page 2 values.
  if (!empty($form_state['page_values'][2])) {
    $form_state['values'] = $form_state['page_values'][2];
  }

  // Set the step.
  $form_state['step'] = 2;
  // Rebuild the form.
  $form_state['rebuild'] = TRUE;
}

/**
 * Submit handler for back button.
 *
 * Since #limit_validation_errors = array() is set, values from page 2
 * will be discarded.
 */
function mymodule_register_back($form, &$form_state) {
  // Load the page 1 values.
  $form_state['values'] = $form_state['page_values'][1];
  // Set the step.
  $form_state['step'] = 1;
  // Rebuild the form.
  $form_state['rebuild'] = TRUE;
}

shariharan’s picture

This worked. Thanks a bunch

Drupal Scavenger

rsharkey’s picture

/**
* @file
* Customizations to the Drupal registration system.
*/
/**
* Implements hook_form_FORM_ID_alter().
*/
function vmb_rules_form_user_register_form_alter(&$form, &$form_state) {
  // Make this a multi-step form.
  if (!empty($form_state['step']) && $form_state['step'] == 2) {
    vmb_rules_register_alter_page_two($form, $form_state);
  }
  else {
    // Otherwise we build page 1.
    vmb_rules_register_alter_page_one($form, $form_state);
  }
}

/**
* Form alter - Step 1 of user registration.
*/
function vmb_rules_register_alter_page_one(&$form, &$form_state) {

  // Set the step.
  $form_state['step'] = 1;
  // Add text for step 1.
  $form['step'] = array(
    '#markup' => '<h3>' . t('Step 1 of 2: Account information') . '</h3>',
    '#weight' => -10,
  );

  $form['intro'] = array(
    '#markup' => '<p>' . t('Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam eu nibh augue. Phasellus justo lectus, imperdiet quis ligula a, porta commodo nisi. Ut iaculis, leo malesuada fringilla volutpat, magna augue eleifend felis, non dignissim orci elit et nulla.') . '</p><p>' . t('Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Quisque lectus lacus, dapibus ac vestibulum ut, vehicula at nibh.') . '</p><p>' . t('Nullam eget sollicitudin neque. Nam lobortis lacinia vehicula. Cras eget est malesuada, porttitor erat ac, tempus eros.') . '</p>',
    '#weight' => -11,
  );

  // Hide the information for step 2.
  $form['field_organizational_users']['#access'] = FALSE;
  $form['field_organizational_vertical']['#access'] = FALSE;
  $form['field_organizational_branches']['#access'] = FALSE;
  $form['field_organizational_remote_user']['#access'] = FALSE;
  $form['devices_prefix']['#access'] = FALSE;
  $form['field_organizational_pc_devices']['#access'] = FALSE;
  $form['field_organizational_laptop_devi']['#access'] = FALSE;
  $form['field_organizational_chromebook_']['#access'] = FALSE;
  $form['field_organizational_tablet_devi']['#access'] = FALSE;
  $form['field_organizational_smartphone_']['#access'] = FALSE;

  $form['field_byod_initiative']['#access'] = FALSE;
  $form['services_prefix']['#access'] = FALSE;
  $form['field_cloud_services_daas']['#access'] = FALSE;
  $form['field_cloud_service_cloud_file_s']['#access'] = FALSE;
  $form['field_cloud_service_saas']['#access'] = FALSE;
  $form['field_organizational_storage']['#access'] = FALSE;
  $form['field_organizational_security']['#access'] = FALSE;

  // Remove the 'captcha' from step 1.
  unset($form['captcha']);
  //$form['captcha']['#access'] = FALSE;
  //$form['captcha']['#captcha_admin_mode'] = TRUE;

  // Set default values (so they work with back button).

  //page 1 system reg fields
  $form['account']['name']['#default_value'] = !empty($form_state['values']['name']) ? $form_state['values']['name'] : '';
  $form['account']['mail']['#default_value'] = !empty($form_state['values']['mail']) ? $form_state['values']['mail'] : '';
  $form['account']['mail']['#description'] = t('A valid e-mail address. All notifications from this system will use this address.');

  //page 1 fields
  $form['field_first_name']['und'][0]['value']['#default_value'] = !empty($form_state['values']['field_first_name']['und'][0]['value']) ? $form_state['values']['field_first_name']['und'][0]['value'] : '';
  $form['field_last_name']['und'][0]['value']['#default_value'] = !empty($form_state['values']['field_last_name']['und'][0]['value']) ? $form_state['values']['field_last_name']['und'][0]['value'] : '';
  $form['field_company']['und'][0]['value']['#default_value'] = !empty($form_state['values']['field_company']['und'][0]['value']) ? $form_state['values']['field_company']['und'][0]['value'] : '';
  $form['field_phone']['und'][0]['value']['#default_value'] = !empty($form_state['values']['field_phone']['und'][0]['value']) ? $form_state['values']['field_phone']['und'][0]['value'] : '';
  $form['field_country']['und']['#default_value'] = !empty($form_state['values']['field_country']['und'][0]['value']) ? $form_state['values']['field_country']['und'][0]['value'] : '';
  $form['field_device_os']['und']['#default_value'] = !empty($form_state['values']['field_device_os']['und'][0]['value']) ? $form_state['values']['field_device_os']['und'][0]['value'] : '';

  //page 2 fields
  //they are not neccessary here, since they will exist on page two.

  // Add a next button.
  $form['actions']['next'] = array(
    '#type' => 'submit',
    '#value' => 'Next',
    '#submit' => array('vmb_rules_register_next'),
  );
  // Remove the 'Create new account' button from step 1.
  unset($form['actions']['submit']);
}

/**
* Form alter - Step 2 of user registration.
*/

function vmb_rules_register_alter_page_two(&$form, &$form_state) {

  // Add text for step 2.
  $form['step'] = array(
    '#markup' => '<h3>' . t('Step 2 of 2: Establishment information') . '</h3>',
    '#weight' => -10,
  );

  // Hide the fields completed on step 1.
  $form['account']['name']['#access'] = FALSE;
  $form['account']['mail']['#access'] = FALSE;
  $form['field_first_name']['#access'] = FALSE;
  $form['field_last_name']['#access'] = FALSE;
  $form['field_device_os']['#access'] = FALSE;
  $form['field_company']['#access'] = FALSE;
  $form['field_phone']['#access'] = FALSE;
  $form['field_country']['#access'] = FALSE;

  //page 1 system reg fields
  $form['account']['name']['#default_value'] = !empty($form_state['values']['name']) ? $form_state['values']['name'] : '';
  $form['account']['mail']['#default_value'] = !empty($form_state['values']['mail']) ? $form_state['values']['mail'] : '';
  $form['account']['mail']['#description'] = t('A valid e-mail address. All notifications from this system will use this address.');

  //page 1 fields
  $form['field_first_name']['und'][0]['value']['#default_value'] = !empty($form_state['values']['field_first_name']['und'][0]['value']) ? $form_state['values']['field_first_name']['und'][0]['value'] : '';
  $form['field_last_name']['und'][0]['value']['#default_value'] = !empty($form_state['values']['field_last_name']['und'][0]['value']) ? $form_state['values']['field_last_name']['und'][0]['value'] : '';
  $form['field_company']['und'][0]['value']['#default_value'] = !empty($form_state['values']['field_company']['und'][0]['value']) ? $form_state['values']['field_company']['und'][0]['value'] : '';
  $form['field_phone']['und'][0]['value']['#default_value'] = !empty($form_state['values']['field_phone']['und'][0]['value']) ? $form_state['values']['field_phone']['und'][0]['value'] : '';
  $form['field_country']['und']['#default_value'] = !empty($form_state['values']['field_country']['und'][0]['value']) ? $form_state['values']['field_country']['und'][0]['value'] : '';
  $form['field_device_os']['und']['#default_value'] = !empty($form_state['values']['field_device_os']['und'][0]['value']) ? $form_state['values']['field_device_os']['und'][0]['value'] : '';


  //page 2 fields

  //for each field on page 2 genrate the default value attribute and pass in the corresponding ['storage'] value if it exists.
  $fields = array(
            'field_organizational_users',
            'field_organizational_vertical',
            'field_organizational_branches',
            'field_organizational_remote_user',
            'field_organizational_pc_devices',
            'field_organizational_laptop_devi',
            'field_organizational_chromebook_',
            'field_organizational_tablet_devi',
            'field_organizational_smartphone_',
            'field_byod_initiative',
            'field_cloud_services_daas',
            'field_cloud_service_cloud_file_s',
            'field_cloud_service_saas',
            'field_organizational_storage',
            'field_organizational_security',
    );

  foreach($fields as $field){
    $form[$field]['und']['#default_value'] = !empty($form_state['storage'][$field]) ? $form_state['storage'][$field] : '';
  }



  //set the prefix label for the devices drop-downs.
  $form['devices_prefix'] = array(
    '#markup' => '<label style="margin-top: 1em;">' . t('7. What percent of your people use each of these devices for access to IT everyday?: ') . '</label>',
    //'#weight' => -10,
  );
  
  //set the prefix label for the services drop-downs.
  $form['services_prefix'] = array(
    '#markup' => '<label style="margin-top: 1em;">' . t('9. Does your organization use any of the following cloud services?') . '</label>',
    //'#weight' => -10,
  );

  // Add a back button.
  $form['actions']['back'] = array(
    '#type' => 'submit',
    '#value' => t('Back'),
    '#submit' => array('vmb_rules_register_back'),
    '#limit_validation_errors' => array(),
  );
   // Adjust the submit button to come last.
  $form['actions']['submit']['#weight'] = 100;
  // Add our own validation handler.
  $form['actions']['submit']['#validate'] = array('vmb_rules_register_validate');
  // Replace default submit value.
  $form['actions']['submit']['#value'] = t('Submit');
  // Add an additional submit handler to the whole form.
  $form['#submit'][] = 'vmb_rules_register_submit';
}

/**
* Validate handler.
*/

function vmb_rules_register_validate(&$form, &$form_state) {
  
}

/**
* Submit handler for user registration form.
*
* Namespace collision demands underscore.
*/

function vmb_rules_register_submit(&$form, &$form_state) {


  
}

/**
* Submit handler for next button.
*
* Capture the values from page one and store them away so they can be used
* at final submit time.
*/

function vmb_rules_register_next($form, &$form_state) {

  // Save the page 1 values.
  $form_state['page_values'][1] = $form_state['values'];

  // Load the page 2 values.
  //if (!empty($form_state['page_values'][2])) {
    //$form_state['values'] = $form_state['page_values'][2];
  //}

  // Set the step.
  $form_state['step'] = 2;
  // Rebuild the form.
  $form_state['rebuild'] = TRUE;
}

/**
* Submit handler for back button.
*
* Since #limit_validation_errors = array() is set, values from page 2
* will be discarded.
*/

function vmb_rules_register_back($form, &$form_state) {

  // Save the page 2 values into the ['storage'] nested array.
  $fields = array(
            'field_organizational_users',
            'field_organizational_vertical',
            'field_organizational_branches',
            'field_organizational_remote_user',
            'field_organizational_pc_devices',
            'field_organizational_laptop_devi',
            'field_organizational_chromebook_',
            'field_organizational_tablet_devi',
            'field_organizational_smartphone_',
            'field_byod_initiative',
            'field_cloud_services_daas',
            'field_cloud_service_cloud_file_s',
            'field_cloud_service_saas',
            'field_organizational_storage',
            'field_organizational_security',
    );

  foreach($fields as $field){
    $form_state['storage'][$field] = !empty($form[$field]['und']['#value']) ? $form[$field]['und']['#value'] : '';
  }

  // Load the page 1 values.
  $form_state['values'] = $form_state['page_values'][1];

  // Set the step.
  $form_state['step'] = 1;
  // Rebuild the form.
  $form_state['rebuild'] = TRUE;
}

Here is a multi-step registration form alter that I got working with mulitple custom fields. Originally my problem was figuring out how to cache the custom fields from page to page before the submission.

I had to add some logic to the "vmb_rules_register_back()" function in order to store the values in the ['storage'] nested array.

It is working now.

Hopefully this will help someone else out.

anawillem’s picture

This helps me out alot. Thank you. You saved me a ton of time.

One outlying issue I am having difficulty with is that I have an Addressfield Autocomplete field that is hooked in with Geofield (on step 1 of my user form) and it just does not want to Autocomplete the addresses, and also does not save the addresses.

Here is my version of your code:

<?php
/**
* Implements hook_form_FORM_ID_alter().
*/
function lex_misc_form_user_register_form_alter(&$form, &$form_state) {
  // Make this a multi-step form.
  if (!empty($form_state['step']) && $form_state['step'] == 2) {
    lex_misc_register_alter_page_two($form, $form_state);
  }
  else {
    // Otherwise we build page 1.
    lex_misc_register_alter_page_one($form, $form_state);
  }
}


/**
* Form alter - Step 1 of user registration.
*/
function lex_misc_register_alter_page_one(&$form, &$form_state) {

  // Set the step.
  $form_state['step'] = 1;
  // Add text for step 1.
  $form['step'] = array(
    '#markup' => '<h1>' . t('Step 1 of 4: Tell us about yourself') . '</h1>',
    '#weight' => -10,
  );

  // Hide the information for step 2.
  $form['field_user_picture']['#access'] = FALSE;
  $form['field_short_bio']['#access'] = FALSE;
  $form['field_user_about']['#access'] = FALSE;
  $form['field_place_of_work']['#access'] = FALSE;
  $form['field_links']['#access'] = FALSE;
  $form['field_user_type']['#access'] = FALSE;

  // Set default values (so they work with back button).

  //page 1 system reg fields
  $form['account']['name']['#default_value'] = !empty($form_state['values']['name']) ? $form_state['values']['name'] : '';
  $form['account']['mail']['#default_value'] = !empty($form_state['values']['mail']) ? $form_state['values']['mail'] : '';
  $form['account']['mail']['#description'] = t('A valid e-mail address. All notifications from this system will use this address.');
  $form['account']['pass']['#value'] = 'pass';
  $form['account']['pass']['#default_value'] = 'pass';
  $form['account']['pass']['#type'] = 'hidden';
  

  //page 1 fields
  $form['field_account_type']['und'][0]['value']['#default_value'] = !empty($form_state['values']['field_account_type']['und'][0]['value']) ? $form_state['values']['field_account_type']['und'][0]['value'] : '';
  $form['field_first_name']['und'][0]['value']['#default_value'] = !empty($form_state['values']['field_first_name']['und'][0]['value']) ? $form_state['values']['field_first_name']['und'][0]['value'] : '';
  $form['field_last_name']['und'][0]['value']['#default_value'] = !empty($form_state['values']['field_last_name']['und'][0]['value']) ? $form_state['values']['field_last_name']['und'][0]['value'] : '';
  $form['field_organization']['und'][0]['value']['#default_value'] = !empty($form_state['values']['field_organization']['und'][0]['value']) ? $form_state['values']['field_organization']['und'][0]['value'] : '';
  $form['field_address']['und'][0]['field_address']['autocomplete']['#default_value'] = !empty($form_state['values']['field_address']['und'][0]['field_address']['autocomplete']) ? $form_state['values']['field_address']['und'][0]['field_address']['autocomplete'] : '';
  
  //page 2 fields
  //they are not neccessary here, since they will exist on page two.

  // Add a next button.
  $form['actions']['next'] = array(
    '#type' => 'submit',
    '#value' => 'Next',
    '#submit' => array('lex_misc_register_next'),
  );
  // Remove the 'Create new account' button from step 1.
  unset($form['actions']['submit']);
}


/**
* Form alter - Step 2 of user registration.
*/

function lex_misc_register_alter_page_two(&$form, &$form_state) {

  // Add text for step 2.
  $form['step'] = array(
    '#markup' => '<h1>' . t('Step 2 of 4: In your own words') . '</h1>',
    '#weight' => -10,
  );

  //page 1 system reg fields
  $form['account']['name']['#default_value'] = !empty($form_state['values']['name']) ? $form_state['values']['name'] : '';
  $form['account']['mail']['#default_value'] = !empty($form_state['values']['mail']) ? $form_state['values']['mail'] : '';
  $form['account']['mail']['#description'] = t('A valid e-mail address. All notifications from this system will use this address.');

  //page 1 fields
  $form['field_account_type']['und'][0]['value']['#default_value'] = !empty($form_state['values']['field_account_type']['und'][0]['value']) ? $form_state['values']['field_account_type']['und'][0]['value'] : '';
  $form['field_first_name']['und'][0]['value']['#default_value'] = !empty($form_state['values']['field_first_name']['und'][0]['value']) ? $form_state['values']['field_first_name']['und'][0]['value'] : '';
  $form['field_last_name']['und'][0]['value']['#default_value'] = !empty($form_state['values']['field_last_name']['und'][0]['value']) ? $form_state['values']['field_last_name']['und'][0]['value'] : '';
  $form['field_organization']['und'][0]['value']['#default_value'] = !empty($form_state['values']['field_organization']['und'][0]['value']) ? $form_state['values']['field_organization']['und'][0]['value'] : '';
  $form['field_address']['und'][0]['field_address']['autocomplete']['#default_value'] = !empty($form_state['values']['field_address']['und'][0]['field_address']['autocomplete']) ? $form_state['values']['field_address']['und'][0]['field_address']['autocomplete'] : '';
 
  // Hide the fields completed on step 1.
  $form['account']['name']['#access'] = FALSE;
  $form['account']['mail']['#access'] = FALSE;
  $form['field_first_name']['#access'] = FALSE;
  $form['field_last_name']['#access'] = FALSE;
  $form['field_organization']['#access'] = FALSE;
  $form['field_address']['#access'] = FALSE;
  $form['field_account_type']['#access'] = FALSE;
  $form['field_user_type']['#access'] = FALSE;
  
  
  //page 2 fields
  //for each field on page 2 genrate the default value attribute and pass in the corresponding ['storage'] value if it exists.
  $fields = array(
      'field_user_picture',
      'field_short_bio',
      'field_user_about',
      'field_place_of_work',
      'field_links',
    );
    
  foreach($fields as $field){
    $form[$field]['und']['#default_value'] = !empty($form_state['storage'][$field]) ? $form_state['storage'][$field] : '';
  }

  // Add a back button.
  $form['actions']['back'] = array(
    '#type' => 'submit',
    '#value' => t('Back'),
    '#submit' => array('lex_misc_register_back'),
    '#limit_validation_errors' => array(),
  );
   // Adjust the submit button to come last.
  $form['actions']['submit']['#weight'] = 100;
  // Add our own validation handler.
  $form['actions']['submit']['#validate'] = array('lex_misc_register_validate');
  // Replace default submit value.
  $form['actions']['submit']['#value'] = t('Next');
  // Add an additional submit handler to the whole form.
  $form['#submit'][] = 'lex_misc_register_submit';
}

/**
* Validate handler.
*/

function lex_misc_register_validate(&$form, &$form_state) {
  
}

/**
* Submit handler for user registration form.
*
* Namespace collision demands underscore.
*/

function lex_misc_register_submit(&$form, &$form_state) {

}


/**
* Submit handler for next button.
*
* Capture the values from page one and store them away so they can be used
* at final submit time.
*/

function lex_misc_register_next($form, &$form_state) {

  // Save the page 1 values.
  $form_state['page_values'][1] = $form_state['values'];

  // Load the page 2 values.
  //if (!empty($form_state['page_values'][2])) {
    //$form_state['values'] = $form_state['page_values'][2];
  //}

  // Set the step.
  $form_state['step'] = 2;
  // Rebuild the form.
  $form_state['rebuild'] = TRUE;
}

/**
* Submit handler for back button.
*
* Since #limit_validation_errors = array() is set, values from page 2
* will be discarded.
*/

function lex_misc_register_back($form, &$form_state) {

  // Save the page 2 values into the ['storage'] nested array.
  $fields = array(
      'field_user_picture',
      'field_short_bio',
      'field_user_about',
      'field_place_of_work',
      'field_links',
    );

  foreach($fields as $field){
    $form_state['storage'][$field] = !empty($form[$field]['und']['#value']) ? $form[$field]['und']['#value'] : '';
  }

  // Load the page 1 values.
  $form_state['values'] = $form_state['page_values'][1];

  // Set the step.
  $form_state['step'] = 1;
  // Rebuild the form.
  $form_state['rebuild'] = TRUE;
}
?>

--
anawillem
http://jellobrain.com

mchar’s picture

Thanks for sharing this code!
I have a little trouble, could you give me a tip please ?
This line of code '#submit' => array('lex_misc_register_back'), is never get called for me, do you have any idea why ? should I change something in the code, does the submit button ("Create new account") of the user registration form interfere somehow ? I am trying to figure out what going on two days now. I have exactly implement all the steps described above but with no luck. The callback function is never get invoked.
Thanks in advance.

Jaypan’s picture

If $form['#submit'] is never called, it means that there is a submit handler attached to the button that is being clicked. You will need to attach the submit function to that button, instead of the form.

mchar’s picture

Thanks for your quick response, I really appreciate that.
I am not an experienced developer though, do you mean this:
$form['actions']['submit']['#submit'] = array('lex_misc_register_next');
If I am not wrong this line adds a submit handler to the submit button ("Create new account"), correct ?

Andy_D’s picture

I found your code is working brilliantly and apart from the fact that I need to work out a three part form (my problem!) I also found out that form_alters render in a specific order. In my case the legal module was rendering after my alter so I was unable to manipulate it. A quick google revealed this. Simply use at the end of your module if required.

/**
 * Implements hook_form_alter().
 */
function my_module_form_alter(&$form, &$form_state, $form_id) {
  // do your stuff
}

/**
 * Implements hook_module_implements_alter().
 *
 * Make sure that our form alter is called AFTER the same hook provided in xxx
 */
function my_module_module_implements_alter(&$implementations, $hook) {
  if ($hook == 'form_alter') {
    // Move my_module_form_alter() to the end of the list. module_implements()
    // iterates through $implementations with a foreach loop which PHP iterates
    // in the order that the items were added, so to move an item to the end of
    // the array, we remove it and then add it.
    $group = $implementations['my_module'];
    unset($implementations['my_module']);
    $implementations['my_module'] = $group;
  }
}
prezaeis’s picture

thank you for the code
can you just tell me what this module will do? will it take the current user registration form and break it in to multiple pages?
what if i have extra fields? how would u define which field goes on which page?

jenlampton’s picture

This module will break the registration form into multiple steps. If you read the code comments you can see where you take the (complete) form, and hide the fields you want only on page 2.

// Hide the information for step 2.
  $form['field_stuff']['#access'] = FALSE;
  $form['field_abc']['#access'] = FALSE;
  $form['field_whatevs']['#access'] = FALSE;

Then on page two, you take the (complete) form and just hide the fields that were completed on page 1

// Hide the fields completed on step 1.
  $form['account']['name']['#access'] = FALSE;
  $form['account']['mail']['#access'] = FALSE;
borngunners’s picture

This is exactly what I want to get done. I am working on a site that requires multiple step registration and role selection. I will like to add other fields if possible. Can you please explain to me which module to modify as stated above.

Thanks

jenlampton’s picture

You will need to create your own, custom module to use this code. There are some good videos on Drupalize.me and buildamodule.com that will walk you through the process of creating your own module if you have not done it before.

One word of caution, please be safe and do not attempt to develop a new module on your live site, build one locally, or on a test or dev copy of your site first, then once you confirm it's working push it to the live site.

borngunners’s picture

I got this error message on the code I copied from above.
Parse error: syntax error, unexpected '.' in /home/asktechs/public_html/job/sites/all/modules/multi_reg/multireg.module on line 94

What does the error message means?

goofus’s picture

Hello,
You might consider starting a new thread. This is a pretty long conversation your add too :)

Next, try using a programmers editor that understands php syntax. Those syntax exceptions happen to everyone, that's why so many programmer editors have built-in syntax checkers. For example, I'm using VIM right now. It takes a little to get used to, but it does syntax checking on almost every computer language I use.

Good Luck :)

brainhell’s picture

Hi! I'm trying to use your code with a profile2 register form whith a lot of fields and i need split up this in four steps. How can i do it?

A_Z_Official’s picture

Hi there, can anyone help me to expand this code to add 4 steps while registration?

Also I want to use profile2 profile form on second step, can anyone give me an example?

Thanks in advance.