cTools wizard.inc is powerful, but suffers from a few DX issues, namely:
1. The huge amount of freedom makes it really easy to mess up big on something relatively small.
2. Very few defaults, and no standard naming conventions for callbacks.
I've done some work on these issues, and think much of it could be integrated into ctools. A (mostly) working nonpublic pilot of these ideas is available here: (for the curious)
http://www.nicklewis.org/wizard_helper.zip
Before actually submitting a patch i wanted to propose the changes first, as there is still a lot I don't know about ctools and the wizard.
PROPOSED PATCH OVERVIEW:
1. implementing a hook like system similar to formAPI's -- there would be two types of hooks, multistep, and single step.
2. providing default values, requiring a minimal number of arrays to be defined
3. automated cache handling between steps ( this may have to default to off for existing wizard form compatibility)
I. HOOKS AND DEFAULTS
Default values would be stored in an array that merges with whatever is defined here. The necessary hooks are defined in the below example of a minimal implementation.
function example_wizard_multistep_form_info() {
return array(
'id' => 'example_multistep_form',
// MULTISTEP HOOKS
// 1. example_multistep_form_cancel(&$form_state)
// 2. example_multistep_form_next(&$form_state)
// 3. example_multistep_form_return(&$form_state)
// 4. example_multistep_form_finish(&$form_state)
'path' => "example-wizard/add/%step",
// note that order is defined in this array as well (we can make that backword compatible)
// i see no reason to have two arrays ideally.
'forms' => array(
//SINGLE STEP HOOKS
'firststep' => array(
1. example_multistep_form_firststep_form(&$form, &$form_state)
- example_multistep_form_firststep_form_submit
- example_multistep_form_firststep_form_validate
'title' => t('Step 1')
),
'secondstep' => array(
'title' => t('Step 2')
),
);
}
Some additional rules:
1. These hooks could be overridden by declaring them explicitly in the form_info array to preserve backward compatibility.
2. If the callback doesn't exist, and no alternative was defined, the module fallsback to a default hook. For example:
/**
* Default next callback
* can be overridden declaring $multistep_id_next
*/
function ctools_wizard_default_next(&$form_state) {
$cache_key = $form_state['cache key'];
$data = &$form_state[$cache_key];
// note that the new cache name key makes these defaults possible
// unless specified it will default to $multistep_id_cache
ctools_object_cache_set($form_state['cache key'], $form_state['cache name'], $data);
}
II. AUTOMATIC CACHING
This is admittedly more ambitious of a suggestion.
1. Prior to the form being build, we could handle cache link this:
if (!$cache) {
$cache = array();
if ($form_state['set cache']) {
$cache = $form_state['set cache'];
}
else {
foreach($form_info['forms'] as $form_step => $params) {
$step_cache = array();
$stephook = $hook.'_'.$form_step;
if ($func = wizard_helper_hook($stephook, 'cache_initialize')) {
// e.g example_mulitstep_form_firststep_cache_initialize for steps that require default values
$func($step_cache);
}
// this form's cache is now in an array that follows the naming conventions in the rest of the system
$cache[$form_step] = $step_cache;
}
// this could be an even higher level cache setup function -- that could override or work on the defaults setup above
if ($func = wizard_helper_hook($hook, 'cache_initialize')) {
$func($cache);
}
}
ctools_object_cache_set($form_state['cache key'], $form_state['cache name'], $cache);
}
$form_state[$cache_key] = $cache;
Because this follows a pattern, its now possible to automatically cache a form step's state:
/**
* Generic submit for forms that have none
*/
function ctools_wizard_step_default_submit(&$form_state) {
// form state knows this
$step = $form_state['step'];
// so with one additional piece of info
$cache_key = $form_state['cache key'];
/// we can do this -- we'll probably want to cleanup junk values that will be in form_state['values']
$form_state[$cache_key][$step] = $form_state['values'];
}
Finally, this pattern can be used for the final saving of a form. This example "finish" step is working really well for me in practice:
/**
* Generic finish callback
*/
function ctools_wizard_default_finish(&$form_state) {
$cache_key = $form_state['cache key'];
foreach($form_state['form_info']['forms'] as $step => $params) {
$subhook = $form_state['hook'].'_'.$step;
// isolate a steps values
$step_cache = $form_state[$cache_key][$step];
if ($func = wizard_helper_hook($subhook, 'step_save')) {
// this is an individual save function for a steps data. Note the return value
$result = $func($step_cache);
}
// this results array is possibly storing ids returned from an individual step's save hook
$results[$step] = $result;
}
// haven't really decided on the name of this hook
// its intention is either to save the form results in one grand function
// or -- as is often the case tie a bunch of a different data objects together
if ($func = wizard_helper_hook($form_state['hook'], 'finish')) {
$func($form_state, $results);
}
}
The way I see it, I think the main advantage of doing this is that it breaks down all the complexity into simple simple steps that follow simple conventions. It also defaults to not requiring additional code, as opposed to the otehr way around.
The main disadvantage I see is that it does force something of a strict convention of how data is stored, loaded, and saved. However, i think for many people, that disadvantage is an advantage.
Thoughts?
| Comment | File | Size | Author |
|---|---|---|---|
| #10 | ctools-wizard-default.patch | 8.6 KB | Nick Lewis |
| #6 | ctools-wizard-default.patch | 4.47 KB | Nick Lewis |
| #5 | ctools-wizard-default.patch | 4.47 KB | Nick Lewis |
| #4 | ctools-wizard-default.patch | 4.46 KB | Nick Lewis |
| #4 | example_wizard.zip | 1.57 KB | Nick Lewis |
Comments
Comment #1
merlinofchaos commentedThe caching method *must* be optional. I have complex requirements in other forms. However, for a more straightforward workflow, I agree that it should exist and be very easy to use.
Form order is another tricky thing, since more complex workflows will not have simple orders, but all of the forms should be defined. This is maybe doable so long as we don't accidentally remove functionality.
I'm all for autogenerating callbacks, but please note that I often have _next and _back as the same callback, but this isn't necessarily always true. Though just overriding that one isn't hard, either.
Comment #2
Nick Lewis commentedsweet i'll get cracking against the latest version on dev.
Comment #3
Nick Lewis commentedFirst pass at a patch. Posting so that I start what I finish.
Autocallbacks, default, and what I'm calling "ezmode" for now included. Fortunately, the actual changes to the ctools_wizard_multistep_form function were very light.
As per requirement auto caching/save callbacks stuff only fires when ezmode = true. Trying to think of a better name then ezmode to switch the automatic behavior on.
There was an 11th hour addition that lets someone specify cache and save callbacks for individual steps in ezmode. This makes it possible for a module to have a common method for adding node forms, e.g.:
***
Some next steps are writing a module that successfully uses all the features.
In addition, I was wondering if there were some wizard forms that you knew of that would make good candidates for me to spot check for bugs I may have introduced.
In the meantime, i'll be cleaning up, documenting, testing, etc...
Comment #4
Nick Lewis commentedHere's a smaller patch that just sets default callbacks, default settings and removes the need for the order array to always be defined. Example implementation test module attached.
Comment #5
Nick Lewis commentedsmall error
Comment #6
Nick Lewis commentedcorrection for _back callback
Comment #7
merlinofchaos commented2 nits:
1) Since the wizard is actually documented, this needs to be reflected in the documentation.
2) dot (concat operator) always has a space around it in my world. Even in the Drupal 6 world, there IS a space between a variable name and a dot. =)
$foo.$baris always wrong =)Comment #8
Nick Lewis commentedtouche.
I can reroll tomorrow.
What do you see as achieving proper documentaiton?
- required vs optional perhaps?
Comment #9
merlinofchaos commentedDocument what the defaults will be if left out in the advanced help file. That ought to cover it.
Comment #10
Nick Lewis commentedDone. Documentation changes were rather light for now. Struggled a bit on how to describe step form names, and eventually settled on $form_info['id']._.$step._form as the clearest way.
Comment #11
merlinofchaos commentedOk, committed. Doesn't look like it should break anything.