Multipage User Registration form

The University of PEI (subcontracted from Dave Kisly) required a complex multipaged user registration form for their CAUBO project. There are a few posts around on creating multipage forms, but most are incorrect, don't apply well to the user registration process or are simply too complex to map to your specific requirements.

We did an initial version of this registration module based on one of these recipe guides but with numerous changes from the client it eventually became a very messy piece of code that was too complex to continue with all the changes required.

Eventually we came across this posting: http://www.isaacsukin.com/news/2010/04/04/multi-page-forms-drupal-6 and thanks a ton to Isaac for posting this as it paved the way for us to come up with a much cleaner module to do what we were looking for.

Should anyone (and from the posts I have seen there have been lots of developers looking for this) be in need of a solution for this... this is our version of what Isaac posted with a few fixes and many additions to show off a much more complex example. The aspects of our module which make it different than Isaac's original posting include the following:

  • use our multipage form as a replacement for the standard user registration form
  • use a single Content Profile node as part of the user registration process
  • split the single CP node amongst the various pages
  • various field limiting functions to limit options for a field used on one page based on values entered in a previous page
  • properly handle required fields - Isaacs original code does not have any required fields on pages other than page 1. If he did, there would be issues when trying to go back to the previous page if those required fields had not yet been filled out.

there are also a few things we do in our module which I won't get into here, such as:

  • custom user registration submit function which combines some of the features of logintoboggan and other modules

Step By Step

This is a step by step explanation of how we go this to work. The attached module also has many comments in that should also help the reader understand how this works.

1. replace standard user registration form

<?php
function caubo_registration_menu_alter(&$items) {
 
$items['user/register']['page arguments'] = array('caubo_user_register_form');
}
?>

2. the custom registration form

this is the main callback which displays the form for all 3 pages of our multipage form

<?php
/**
* This is our form callback.
* - taken from http://www.isaacsukin.com/news/2010/04/04/multi-page-forms-drupal-6
*/
function caubo_user_register_form(&$form_state) {
 
// $form_state['storage']['step'] keeps track of what page we're on.
 
if (!isset($form_state['storage']['step'])) {
   
$form_state['storage']['step'] = 1;
  }

 
//Don't lose our old data when returning to a page with data already typed in.
 
$default_value = '';
  if (isset(
$form_state['storage']['values'][$form_state['storage']['step']])) {
   
$default_value = $form_state['storage']['values'][$form_state['storage']['step']];
  }

  switch (
$form_state['storage']['step']) {
    case
1:
     
drupal_set_title(t("User Registration"));
    
     
// add Agree to Terms checkbox
     
$form['agreetoterms'] = array(
       
'#type' => 'checkbox',
       
//'#title' => '<strong>' . t('Agree to Terms') . '</strong>',    
       
'#title' => t('Agree to Terms'), 
       
'#description' => t('Please check this as acceptance of our Terms and Conditions of use'),
       
'#weight' => -15,
       
'#attributes' => array('class' => 'required'),
       
'#required' => true,
      );
     
$form = array_merge($form, caubo_get_terms_and_conditions());
    
     
/**
      *  add User Registration form
      *     - not sure advantage of using user_edit_form. it does some other processing but also leaves us with a fieldset wrapper we don't want
      *     - both seem to submit fine so use retrieve_ form for now
      */
      //$form = array_merge($form, user_edit_form($form_state, NULL, NULL, TRUE));
     
$form = array_merge($form, drupal_retrieve_form('user_register', $form_state));
    
     
/*  this doesnt display the radios for some reason ??
      // add Content Profile - Active in Community
      $cpform = _caubo_register_get_cprofile_form($form_state, $default_value);
      // remove everything except Active in Com field
      foreach($cpform as $key => $field) {
        if ($key != 'field_in_community') unset($cpform[$key]);
      }
      $form = array_merge($form, $cpform);
      $form['field_in_community']['#weight'] = 10;
      */
    
      // add fake CP field since the way above doesnt work
     
$form['fake_in_community'] = array(        // don't use std CCK field name as CCK doesn't like this
       
'#type' => 'radios',
       
'#title' => t('Active in Community'), 
       
'#description' => t('By clicking yes, it means that you wish to participate in the CAUBO Cybercommunity and are willing to have your name displayed in
          various "invitation" list / search interfaces. This does not mean that your name will be visible to non-authenticated users.'
),
       
'#options' => array(t("No"), t("Yes")),
       
'#weight' => 10,
       
'#required' => true,
      );
          
     
// add default values for when we go BACK to this page
     
$form['name']['#default_value'] = isset($default_value['name']) ? $default_value['name'] : '';
     
$form['mail']['#default_value'] = isset($default_value['mail']) ? $default_value['mail'] : '';
     
$form['agreetoterms']['#default_value'] = isset($default_value['agreetoterms']) ? $default_value['agreetoterms'] : 0;
     
$form['fake_in_community']['#default_value'] = isset($default_value['fake_in_community']) ? $default_value['fake_in_community']: '';
 
    
     
// get rid of Std Create Account button
     
unset($form['submit']);   
    
     
// add or Agree to Terms and email domain validators
     
$form['#validate'][] = '_caubo_agreeterms_validate';
     
$form['#validate'][] = '_caubo_registration_email_domain_validate';
    
      break;
  
    case
2:
     
drupal_set_title(t("User Registration - Enter Profile Information"));
    
     
// add Content Profile
     
$form = _caubo_register_get_cprofile_form($form_state, $default_value);
     
// remove Topics field
     
unset($form['taxonomy']);
      unset(
$form['field_in_community']);
    
     
// remove std node submit handler so we go to our submit hook
     
unset($form['#submit']);
    
     
// limit Orgs
     
global $pass_email_address;
     
$pass_email_address = $form_state['values']['mail'];
     
$form['field_organization']['#pre_render'] = array('_caubo_limit_organizations');
    
     
// get rid of Std node buttons
     
unset($form['buttons']);

      break;
 
    case
3:
     
drupal_set_title(t("User Registration - Areas of Interest and Preferred Title"));
    
     
// add Content Profile - Topics
     
$form = _caubo_register_get_cprofile_form($form_state, $default_value);
     
// remove everything except Topics field
     
foreach($form as $key => $field) {
        if (
$key != 'taxonomy') unset($form[$key]);
      }
    
     
// add Role selection    
     
_caubo_registration_roles($form, $form_state);
    
     
// load default values
     
$form['roles']['#default_value'] = isset($default_value['roles']) ? $default_value['roles'] : '';
     
$form['taxonomy'][1]['#default_value'] = isset($default_value['taxonomy']) ? $default_value['taxonomy'][1] : '';    
    
     
$form['#content_profile_registration_use_types']['profile'] = "Profile";
     
$form['#content_profile_weights'] = array(); 

     
$form['#validate'][] = 'caubo_user_register_validate'
     
$form['#validate'][] = 'user_register_validate';

      unset(
$form['#submit']);
     
$form['#submit'][] = 'caubo_user_register_form_submit';
     
// would like to add these here but then no way to stop then from processing if we don't hit Finish (i.e. if we hit Previous)
      //    -
      //$form['#submit'][] = 'caubo_user_register_save';
      //$form['#submit'][] = 'content_profile_registration_user_register_submit';
    
     
break;
  }

 
//Depending on what page we're on, show the appropriate buttons.
 
if ($form_state['storage']['step'] > 1) {
   
$form['previous'] = array(
     
'#type' => 'submit',
     
'#value' => t('<< Previous'),
     
'#weight' => 50,
    );
  }
  if (
$form_state['storage']['step'] != 3) {
   
$form['next'] = array(
     
'#type' => 'submit',
     
'#value' => t('Continue >>'),
     
'#weight' => 51,
    );
  }
  else {
   
$form['finish'] = array(
     
'#type' => 'submit',
     
'#value' => t('Finish'),
     
'#weight' => 52,
    );
  }
 
 
// add to ALL pages
 
  // add validate function so we can remove the other validate functions if we are hitting BACK button
 
$form['#validate'][] = '_caubo_register_remove_validate';
 
  return
$form;
}
?>

Some explanation:

Page 1

  • use drupal_retrieve_form('user_register', $form_state) to pull in the user registration form
  • add various custom fields above and below the user registration field
  • tried adding one of the CP fields (field_in_community) the way we do on Page2 but for some reason this didn't work; so we add a fake field which we then need to handle at the end when we submit the form
  • add default values (mostly drawn from Isaac's original concept) so that the fields remain populated when going back to a previously filled in form
  • unset($form['submit']);  to remove the standard Create Account button that comes with standard Drupal user register form
  • add a couple custom validate functions as we want to ensure:
    • Agree to Terms is checked (we originally required our validator function because of a bug in FormsAPI where setting #required for checkboxes does not work correctly. Another solution is to enable the checkbox_validate module which fixes this bug (fixed in D7).
    • Email validation to ensure that the email domain entered by the user matches one of those in our system (since this project is intended for faculty and students of Canadian colleges and universities; only users with email domains matching these schools may register).

Page 2

  • pull in the Content Profile node but remove the taxonomy (used on page 3) and one other field (used on Page 1)
  • custom function to remove some of the options of one of our CP fields. Since it is a select list we must use a #pre_render to remove the options after they have been added
  • remove the standard node Submit, Preview buttons
<?php
$form
['field_organization']['#pre_render'] = array('_caubo_limit_organizations');
?>

Page 3

  • add in the taxonomy field from our CP node (by running same routine as on Page 2 to pull in the entire CP node; but then loop through and remove all but the taxonomy field)
  • add another custom field which has options based on settings from Page 2
  • remove ALL the user submit functions which various modules may have added and replace with just our custom submit function. This is a bit messy in that it prevents us from adding other user modules in the future which may want to do something when we register a user. The reason for doing this is that these submit handlers will fire when the Previous button is hit. We therefore need to run these functions from within our submit function once we have hit the Finish button (and not if we hit the Previous button).

All Pages

  • add various buttons depending on what page we are on
  • add special validate function to the end of the validate chain
<?php
$form
['#validate'][] = '_caubo_register_remove_validate';
>
<?
php
function _caubo_register_remove_validate(&$form, &$form_state) {
  if (
$form_state['clicked_button']['#id'] == "edit-previous") {
   
form_set_error(null, null, true);     //clear that we had errors
   
drupal_get_messages(null, true);  // and remove msg we're about to get
 
}
}
?>

The remove validate function is a very cool trick to allow us to go back a page without worrying about having entered content into any required fields. It basically just clears any errors and the messages from those errors that were encountered by any other validate functions previous to this one (which since we added this to the end of the chain; should be all of them).

.. more to follow