The mere words "multistep form" once gave me a feeling of dread. There are several techniques (arguably hacks) that enable multistep forms in drupal 6. However, if you've ever used them, you'll know that they are a not techniques for the faint of heart.
While Merlinofchaos's multistep form wizard is not for the faint of heart either, I will say I found programming the forms to be fun. The setup takes a bit of focus, but after that, writing the steps is almost too easy.
Chaos Tool's wizard.inc is distinct:

Here's a Live Demo of the Wombat Deployment tool I wrote that uses the wizard. Only impressive in how easily it was written, and how easily i could write a 3rd, 4th, 5th, or 20 steps following the same pattern [ the subject of future posts are hinted in a rogue modal.inc file in the download.]
To get started building multistep forms, follows these steps.
The full code, and detailed doc on the $form_info array will only be made available to freaks who click the "read more" link.
/*----- PART I CTOOLS WIZARD IMPLMENTATION ----- */
/**
* menu callback for the multistep form
*/
function wombat_wizard() {
// merlin hints that there's a better way to figure out the step
$step = arg(1);
// required includes for wizard
ctools_include('wizard');
ctools_include('object-cache');
// *** SETUP ARRAY multistep setup ****
// these are defined in some docs at end of article
$form_info = array(
'id' => 'wombat_basic',
'path' => "wombat/%step",
'show trail' => TRUE,
'show back' => TRUE,
'show cancel' => true,
'show return' =>false,
'next text' => 'Proceed to next step',
'next callback' => 'wombat_basic_add_subtask_next',
'finish callback' => 'wombat_basic_add_subtask_finish',
'return callback' => 'wombat_basic_add_subtask_finish',
'cancel callback' => 'wombat_basic_add_subtask_cancel',
// this controls order, as well as form labels
'order' => array(
'create' => t('Step 1: Create Wombat'),
'deploy' => t('Step 2: Deploy Wombat'),
),
// here we map a step to a form id.
'forms' => array(
// e.g. this for the step at wombat/create
'create' => array(
'form id' => 'wombat_add_form'
),
'deploy' => array(
'form id' => 'wombat_deployment_form'
),
),
);
// *** SETTING THE FORM UP FOR MULTISTEP *** //
$form_state = array(
'cache name' => NULL,
);
// no matter the step, you will load your values from the callback page
$wombat = wombat_basic_get_page_cache(NULL);
if (!$wombat) {
// set form to first step -- we have no data
$step = current(array_keys($form_info['order']));
$wombat = new stdClass();
// all battlewombats are fuzzy, fyi
$wombat->fur_texture = 'Very fuzzy';
// ** set the storage object so its ready for whatever comes next
ctools_object_cache_set('wombat_basic', $form_state['cache name'], $wombat);
}
//THIS IS WHERE WILL STORE ALL FORM DATA
$form_state['wombat_obj'] = $wombat;
// and this is the witchcraft that makes it work
$output = ctools_wizard_multistep_form($form_info, $step, $form_state);
return $output;
}
Here's Earl Miles' docs on the $form_info array (found in the advanced help of chaos tools)
/*-------------------------- The Two Form Steps ---------------------- */
/**
* All forms within this wizard will take $form, and $form_state by reference
* note that the form doesn't have a return value.
*/
function wombat_add_form(&$form, &$form_state) {
$wombat = &$form_state['wombat_obj'];
$form['name'] = array(
'#type' => 'textfield',
'#required' => 1,
'#title' => 'Wombat name',
'#default_value' => $wombat->name,
);
$form['temperment'] = array(
'#title' => 'Temperment',
'#type' => 'radios',
'#required' => 1,
'#default_value' => $wombat->temperment,
'#options' => array(
'docile' => 'Docile',
'irritable' => 'Irritable',
'dangerous' => 'Dangerous',
)
);
// probably important -- i'm continuing to investigate
$form_state['no buttons'] = TRUE;
}
/**
* A Pretty typical form validate callback
*/
function wombat_add_form_validate(&$from, &$form_state) {
if ($form_state['values']['name'] == 'billy') {
form_set_error('name', 'No wombat is allowed to be named billy!');
}
}
/**
* KEY CONCEPT: generally, you will never save data here -- you will simply add values to the
* yet to be saved object being fed into the ctools cache
*
* Using this pattern -- you will likely want to save within $form_info['finish callback'];
*/
function wombat_add_form_submit(&$from, &$form_state) {
$submitted = $form_state['values'];
$save_values = array('name', 'temperment');
// maybe don't imitate this foreach
foreach($save_values as $value) {
// set the values in the cache object -- it gets passed back to the next step
// because of all that work we did in the form_info array
$form_state['wombat_obj']->$value = $submitted[$value];
}
}
/* step two is identical more or less */
function wombat_deployment_form(&$form, &$form_state) {
$wombat = &$form_state['wombat_obj'];
$dutys = array(
'fuzzball' => 'Provide a fuzzball',
'noise-maker' => 'Make irritating noises'
);
/* we give different duty options based on wombat temper */
switch($wombat->temperment) {
case 'docile':
// only docile wombats allow themselves to become fatballs
$dutys['fatball'] = 'Sit around and be fat';
break;
case 'irritable':
$dutys['menace'] = 'Basic Menace Duties';
break;
case 'dangerous':
$dutys['menace'] = 'Basic Menace Duties';
$dutys['scratch'] = "Scratch warfare";
$dutys['bite'] = "Bite warfare";
break;
}
$form['name'] = array(
'#type' => 'item',
'#title' => 'Wombat name',
'#value' => $wombat->name,
);
$form['temper'] = array(
'#type' => 'item',
'#title' => 'Temperment',
'#value' => $wombat->temperment,
);
$form['duties'] = array(
'#type' => 'checkboxes',
'#tree' => true,
'#options' => $dutys,
'#title' => 'Duties',
'#value' => $wombat->duties,
'#required' => 1,
);
$form_state['no buttons'] = TRUE;
}
/**
* Same idea as previous steps submit
*/
function wombat_deployment_form_submit(&$form, &$form_state) {
$form_state['wombat_obj']->duties = $form_state['values']['duties'];
}
These are the callback functions fire when someone clicks a button and the form passes validation.
/*----PART 3 FORM BUTTON CALLBACKS ---------------------- */
/**
* Callback generated when the multistep form is complete
* this is where you'd normally save. in this case, drupal_set_message just squaks something
*/
function wombat_basic_add_subtask_finish(&$form_state) {
$wombat = &$form_state['wombat_obj'];
drupal_set_message('Wombat '.$wombat->name.' successfully deployed. Wombat will be assigned the following duties: '.implode(",", $wombat->duties)).'.';
// Clear the cache
wombat_basic_clear_page_cache($form_state['cache name']);
$form_state['redirect'] = 'wombat';
}
/**
* Callback for the proceed step
*
*/
function wombat_basic_add_subtask_next(&$form_state) {
// get wombat
$wombat = &$form_state['wombat_obj'];
// set wombat in cache... pretty simple
$cache = ctools_object_cache_set('wombat_basic', $form_state['cache name'], $wombat);
}
/**
* Callback generated when the 'cancel' button is clicked.
*
* All we do here is clear the cache.
* redirect them to where they started
* and call them a coward
*/
function wombat_basic_add_subtask_cancel(&$form_state) {
ctools_object_cache_clear('wombat_basic', $form_state['cache name']);
$form_state['redirect'] = 'wombat';
drupal_set_message('Coward.');
}
/*----PART 4 CTOOLS FORM STORAGE HANDLERS -- these usually don't have to be very unique i think some of them are unused.. [ :- ) ]---------------------- */
/**
* Remove an item from the object cache.
*/
function wombat_basic_clear_page_cache($name) {
ctools_object_cache_clear('wombat_basic', $name);
}
/**
* Get the cached changes to a given task handler.
* (Earl wrote that, not me...)
*/
function wombat_basic_get_page_cache($name) {
$cache = ctools_object_cache_get('wombat_basic', $name);
return $cache;
}
This is about as *fresh* techniques come. I've only figured it out as far as writing out a very generalized example to build from. I'm sure many of you have a better idea of how such a pattern should be structured. Or perhaps you're completely lost (probably my fault). In any case, bringing a process to organize Drupal's gaggle of disconnected forms into something monkey brains can comprehend is * IMPORTANT*. I think multistep forms would be everywhere if they weren't so difficult to do. I think this tool is the practical way to tackle those difficulties. I actually enjoy writing multistep forms using this tool. I never thought i'd say that...
The API of wizard.inc is TBD. On IRC Merlin noted that he would have put more effort into the DX of the wizard had he not been the only person using it. Obviously, that conversation should move to move to the ctools queue.
*Most muppets can be rid of by clearing all caches.... ... but not all...
| Attachment | Size |
|---|---|
| wombat.zip | 6.47 KB |
Comments
Newbie question
Hi there,
I'm kinda new to drupal's Form API. Sorry :)
I must make a webform with multistep, it's a basic domain name registration form. First it asks for the domain name, then make some checks (whois) to see if the domain is available, and upon this test, if the domain is 'free' it provides a link to register it, otherwise it asks the user if he's the owner of the domain and if he wants to transfer it to that registrar.
Now, on your form, i don't understand 'how' you can make 'logical' checks between the steps, like 'step2' is displayed only after some checks have been made on 'step1'.
Would that be possible with your 'technique' ? Or what can be the 'easiest way to do it.
Glad if you could help ...
How does this differ from Multistep Module
I've used multistep module created by vkareh on a few sites and for the most part have been satisfied with creating multistep forms in a GUI with a few simple clicks. Though I have noticed an outstanding issue w/ cck date field and would be interested to see if this module has avoided that issue.
Aside from merlinofchaos's work alway being incredible and well thought out, how do you feel this module improves on providing multistep forms? Do you think this module needs to be expanded to include a GUI before it's more widely used?
Great write-up, I will be using this as a reference when I implement this on future sites.
Lots of differences, most
Lots of differences, most importantly, its not in any way connected to CCK and while it might power a client facing multistep form building gui -- it don't think it would actually provide one itself.
To my knowledge is only been used to build new forms, not alter existing ones -- though come to think of it, i think altering a node form into using this would be a rather interesting test of it.
Date
After checking the issue queue for multistep it appears the Date bug has been fixed.
thanks
I really like when developers are writing about ctools. As I see most of it functionality should be in core, because it will be used by more and more modules..
but. how many errors do you see in this line?
drupal_set_message('Wombat '.$wombat->name.' successfully deployed. Wombat will be assigned the following duties: '.implode(",", $wombat->duties)).'.';I actually thought it was a
I actually thought it was a rather impressive way to "squawk something".
fix
<?phpdrupal_set_message(t('Wombat @wombat-name successfully deployed. Wombat will be assigned the following duties: @duties.', array('@wombat-name' => $wombat->name, '@duties' => implode(",", $wombat->duties))));
?>
fixed:
1. wombat name (user-submitted content) sanitized
2. message translatable
3. dot at the end of sentence