Khanh Hoang - Kenn
Kenn is a user experience designer and front end developer who enjoys creating beautiful and usable web and mobile experiences.
This article provides a step-by-step tutorial for creating a custom, multistep registration form via the Ctools Form Wizard in Drupal 7. If you'd prefer to solely use the core Form API, take a look at Building a Multistep Registration Form in Drupal 7, a previous blog post. In the interest of saving time, I'm going to be lifting some text directly from that post, given that there are a number of overlapping tasks.
Why use the Chaos Tools module to build a multistep form? Well, Ctools offers a number of tools that build upon the core Form API, allowing you to create a multistep form faster. This includes providing a method for caching data in between steps, adding 'next' and 'back' buttons with associated callbacks, generating a form breadcrumb, etc.
First things first— create a new, empty, custom module. In this example, the module will be named grasmash_registration. In the interest of reducing our bootstrapping footprint and keeping things organized, we're also going to create an include file. This will store the various construction and helper functions for our form. Let's name it grasmash_registration_ctools_wizard.inc.
We'll start by defining a "master" ctools form wizard callback. This will define all of the important aspects of our multistep form, such as the child form callbacks, titles, display settings, etc. Please take a look at the help document packaged with ctools in ctools/help/wizard.html for a full list of the available parameters.
/** * Create callback for standard ctools registration wizard. */ function grasmash_registration_ctools_wizard($step = 'register') { // Include required ctools files. ctools_include('wizard'); ctools_include('object-cache'); $form_info = array( // Specify unique form id for this form. 'id' => 'multistep_registration', //Specify the path for this form. It is important to include space for the $step argument to be passed. 'path' => "user/register/%step", // Show breadcrumb trail. 'show trail' => TRUE, 'show back' => FALSE, 'show return' => FALSE, // Callback to use when the 'next' button is clicked. 'next callback' => 'grasmash_registration_subtask_next', // Callback to use when entire form is completed. 'finish callback' => 'grasmash_registration_subtask_finish', // Callback to use when user clicks final submit button. 'return callback' => 'grasmash_registration_subtask_finish', // Callback to use when user cancels wizard. 'cancel callback' => 'grasmash_registration_subtask_cancel', // Specify the order that the child forms will appear in, as well as their page titles. 'order' => array( 'register' => t('Register'), 'groups' => t('Connect'), 'invite' => t('Invite'), ), // Define the child forms. Be sure to use the same keys here that were user in the 'order' section of this array. 'forms' => array( 'register' => array( 'form id' => 'user_register_form' ), 'groups' => array( 'form id' => 'grasmash_registration_group_info_form', // Be sure to load the required include file if the form callback is not defined in the .module file. 'include' => drupal_get_path('module', 'grasmash_registration') . '/grasmash_registration_groups_form.inc', ), 'invite' => array( 'form id' => 'grasmash_registration_invite_form', ), ), ); // Make cached data available within each step's $form_state array. $form_state['signup_object'] = grasmash_registration_get_page_cache('signup'); // Return the form as a Ctools multi-step form. $output = ctools_wizard_multistep_form($form_info, $step, $form_state); return $output; }
As you can see, our registration form will have threes steps:
These have been respectively titled "Register," "Connect," and "Invite."
You should also see that we have referenced a number of, as of yet, non-existent callback functions, as well as a cache retreival function. Let's talk about that cache function first, then look at the callbacks.
Ctools provides a specialized Object Cache feature that allows us to store arbitrary, non-volatile data objects. We will use this feature to store user-submitted form values in between the form's multiple steps. Once the entire form has been completed, we will use that data for processing.
To efficiently utilize the Object cache, we will define a few wrapper functions. These wrapper function will be used to create, retreive, and destroy cache objects.
/** * Retreives an object from the cache. * * @param string $name * The name of the cached object to retreive. */ function grasmash_registration_get_page_cache($name) { ctools_include('object-cache'); $cache = ctools_object_cache_get('grasmash_registration', $name); // If the cached object doesn't exist yet, create an empty object. if (!$cache) { $cache = new stdClass(); $cache->locked = ctools_object_cache_test('grasmash_registration', $name); } return $cache; } /** * Creates or updates an object in the cache. * * @param string $name * The name of the object to cache. * * @param object $data * The object to be cached. */ function grasmash_registration_set_page_cache($name, $data) { ctools_include('object-cache'); $cache = ctools_object_cache_set('grasmash_registration', $name, $data); } /** * Removes an item from the object cache. * * @param string $name * The name of the object to destroy. */ function grasmash_registration_clear_page_cache($name) { ctools_include('object-cache'); ctools_object_cache_clear('grasmash_registration', $name); }
Now, we will define the various callbacks that were referenced in our $form_info array. These callbacks are executed when a user clicks the 'next', 'cancel', or 'finish' buttons in the multi-step form.
/** * Callback executed when the 'next' button is clicked. */ function grasmash_registration_subtask_next(&$form_state) { // Store submitted data in a ctools cache object, namespaced 'signup'. grasmash_registration_set_page_cache('signup', $form_state['values']); } /** * Callback executed when the 'cancel' button is clicked. */ function grasmash_registration_subtask_cancel(&$form_state) { // Clear our ctools cache object. It's good housekeeping. grasmash_registration_clear_page_cache('signup'); } /** * Callback executed when the entire form submission is finished. */ function grasmash_registration_subtask_finish(&$form_state) { // Clear our Ctool cache object. grasmash_registration_clear_page_cache('signup'); // Redirect the user to the front page. drupal_goto('<front>'); }
These forms comprise the individual steps in the multistep form.
function grasmash_registration_group_info_form($form, &$form_state) { $form['item'] = array( '#markup' => t('This is step 2'), ); return $form; } function grasmash_registration_invite_form($form, &$form_state) { $form['item'] = array( '#markup' => t('This is step 3'), ); return $form; }
You can use all of the magic of the Form API with your child forms, including separate submit and validation handlers for each step.
Now for the tricky part— we're going to override the Drupal core user registration form with our multistep ctools form, making user registration the first step.
We will do this by modifying the menu router item that controls the 'user/register' path via hook_menu_alter(). By default, the 'user/register' path calls drupal_get_form() to create the registration form. We're going to change that so that it calls our ctools multistep form callback instead.
Note, all hook implementations should be place in your .module file.
/** * Implements hook_menu_alter(). */ function grasmash_registration_menu_alter(&$items) { // Ctools registration wizard for standard registration. // Overrides default router item defined by core user module. $items['user/register']['page callback'] = array('grasmash_registration_ctools_wizard'); // Pass the "first" step key to start the form on step 1 if no step has been specified. $items['user/register']['page arguments'] = array('register'); $items['user/register']['file path'] = drupal_get_path('module', 'grasmash_registration'); $items['user/register']['file'] = 'grasmash_registration_ctools_wizard.inc'; return $items; }
We will also need to define a new menu router item to handle the subsequent steps of our multistep form. E.g., user/register/%step:
/** * Implements hook_menu(). */ function grasmash_registration_menu() { $items['user/register/%'] = array( 'title' => 'Create new account', 'page callback' => 'grasmash_registration_ctools_wizard', 'page arguments' => array(2), 'access callback' => 'grasmash_registration_access', 'access arguments' => array(2), 'file' => 'grasmash_registration_ctools_wizard.inc', 'type' => MENU_CALLBACK, ); return $items; }
Lastly, we need to make a slight alteration to the user_register_form. It will now have at least two submit handlers bound to it: user_register_submit, and ctools_wizard_submit. We need to make sure that the user_register_submit callback is called first!
/** * Implements hook_form_FORM_ID_alter(). */ function hook_form_user_register_form_alter(&$form, &$form_state) { $form['#submit'] = array( 'user_register_submit', 'ctools_wizard_submit', ); }
That's it! You should now be able to navigate to user/register and see the first step of your multistep form. Subsequent steps will take you to user/register/[step-name].
Now, for a bonus snippet snack, here's how you can take the first step of your form and put it into a block!
/** * Implements hook_block_info(). */ function grasmash_registration_block_info() { $blocks['register_step1'] = array( 'info' => t('Grasmash Registration: Step 1'), 'cache' => DRUPAL_NO_CACHE, ); return $blocks; } /** * Implements hook_block_view(). * * This hook generates the contents of the blocks themselves. */ function grasmash_registration_block_view($delta = '') { switch ($delta) { case 'register_step1': $block['subject'] = 'Create an Account'; $block['content'] = grasmash_registration_block_contents($delta); break; } return $block; } /** * A module-defined block content function. */ function grasmash_registration_block_contents($which_block) { global $user; $content = ''; switch ($which_block) { case 'register_step1': if (!$user->uid) { module_load_include('inc', 'grasmash_registration', 'grasmash_registration_ctools_wizard'); return grasmash_registration_ctools_wizard('register'); } break; } }