Multi-step Forms in Drupal 6 using variable functions
I recently had to write a multi-step form in Drupal 6. Of course, I turned to documentation to see how others are doing it. Pro Drupal Development offers the basics, so do the 5 to 6 upgrade notes, and others. I felt that many approaches suffered from design flaws that made the code cumbersome to manage beyond a couple steps. I set out to develop a multi-step form method with the following goals:
- One form builder with nested conditional statements is difficult to manage, each step should be its own form array function
- Steps shouldn't be numbered, e.g. to move to the next step don't
$form_state['storage']['step']++
- Each step should be able to have its own validate and submit handlers
- Steps should be form alterable
As far as Form API knows, there is one form builder and thus one validation and submit function. The key is, each piece (builder, validate, and submit function) directs to sub-functions that perform during the appropriate step. Value elements are heavily used to control flow by informing the system what the next step is and for handling step validation and submit.
If you're not too familiar with Drupal's Form API watch this excellent intro video from my friend Chris Shattuck at BuildAModule.com
Let's look at some pseudo-code to explain the main idea of what I'm doing.
We get started with drupal_get_form('main_builder')<p>function main_builder(form_array) {<br> Check if we've set our next step<br> <br> If we have call that step's function and <br> return it's Form array<br> <br> If we don't have a step than we're at the <br> beginning of our form<br> Call and return first_step()<br>}</p><p>function first_step(form_array) {<br> Build the form elements we want the user to enter</p><p> Define what the next step from this one is<br> form_array['next_step'] = 'a_single_step';<br> <br> return form_array;<br>}</p><p>function a_single_step(form_array) {<br> Build the form elements we want the user to enter<br> <br> Define what the next step from this one is<br> form_array['next_step'] = 'next_step';</p><p> return form_array; <br>}</p><p>function main_builder_submit(form_array) {<br> Store submitted form values</p><p> Set next step<br>}</p>
The advantage here is each step knows the next step and each step is contained within its own function, allowing for easy modification. The main form builder function dispatches to each individual step to build its own form array. The form submit handler stores submitted values in form storage per usual.
Let's look at some Drupal code now.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #FF8000">// We call drupal_get_form('multistep_form')<br>// elsewhere, such as an implementation of hook_menu().<p></p></span><span style="color: #007700">function </span><span style="color: #0000BB">multistepform_form</span><span style="color: #007700">(</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">) {<br> if (!empty(</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'storage'</span><span style="color: #007700">][</span><span style="color: #DD0000">'step'</span><span style="color: #007700">])) {<br> </span><span style="color: #0000BB">$function </span><span style="color: #007700">= </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'storage'</span><span style="color: #007700">][</span><span style="color: #DD0000">'step'</span><span style="color: #007700">];<br> return </span><span style="color: #0000BB">$function</span><span style="color: #007700">(</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">);<br> }<br> else {<br> return </span><span style="color: #0000BB">_multistepform_form_start</span><span style="color: #007700">(); <br> }<br>}<p>function </p></span><span style="color: #0000BB">_multistepform_form_start</span><span style="color: #007700">() {<br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'name'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'#type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'textfield'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#title' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Name'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'#required' </span><span style="color: #007700">=> </span><span style="color: #0000BB">TRUE</span><span style="color: #007700">,<br> );<p> </p></span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'continue'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'#type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'submit'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#value' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Continue'</span><span style="color: #007700">,<br> );<br> </span><span style="color: #FF8000">// Our special value elements.<br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'this_step'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'#type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'value'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#value' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'start'</span><span style="color: #007700">,<br> );<br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'step_next'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'#type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'value'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#value' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'_multistepform_form_food'</span><span style="color: #007700">,<br> );<br> return </span><span style="color: #0000BB">$form</span><span style="color: #007700">;<br>}<p>function </p></span><span style="color: #0000BB">_multistepform_form_food</span><span style="color: #007700">(</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'food'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'#type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'textfield'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#title' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Food'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'#required' </span><span style="color: #007700">=> </span><span style="color: #0000BB">TRUE</span><span style="color: #007700">,<br> );<br> <br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'continue'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'#type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'submit'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#value' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Continue'</span><span style="color: #007700">,<br> );<br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'this_step'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'#type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'value'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#value' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'food'</span><span style="color: #007700">,<br> );<br> return </span><span style="color: #0000BB">$form</span><span style="color: #007700">;<br>}<p>function </p></span><span style="color: #0000BB">multistepform_form_submit</span><span style="color: #007700">(</span><span style="color: #0000BB">$form</span><span style="color: #007700">, &</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">) {<br> if (empty(</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'storage'</span><span style="color: #007700">])) {<br> </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'storage'</span><span style="color: #007700">] = array();<br> </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'storage'</span><span style="color: #007700">][</span><span style="color: #DD0000">'values'</span><span style="color: #007700">] = array();<br> }<br> </span><span style="color: #FF8000">// Store submitted form values<br> </span><span style="color: #0000BB">$this_step </span><span style="color: #007700">= </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'values'</span><span style="color: #007700">][</span><span style="color: #DD0000">'this_step'</span><span style="color: #007700">];<br> </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'storage'</span><span style="color: #007700">][</span><span style="color: #DD0000">'values'</span><span style="color: #007700">][</span><span style="color: #0000BB">$this_step</span><span style="color: #007700">] = </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'values'</span><span style="color: #007700">];<p> </p></span><span style="color: #FF8000">// Set up next step.<p> </p></span><span style="color: #007700">if (!empty(</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'values'</span><span style="color: #007700">][</span><span style="color: #DD0000">'step_next'</span><span style="color: #007700">])) {<br> </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'storage'</span><span style="color: #007700">][</span><span style="color: #DD0000">'step'</span><span style="color: #007700">] = </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'values'</span><span style="color: #007700">][</span><span style="color: #DD0000">'step_next'</span><span style="color: #007700">];<br> }<br> else {<br> </span><span style="color: #FF8000">// Form complete!<br> </span><span style="color: #0000BB">drupal_set_message</span><span style="color: #007700">(</span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Complete.'</span><span style="color: #007700">));<br> }<br>}<br></span><span style="color: #0000BB">?></span></span>
See the full example in the attachment
As you can see, this method uses special form value elements to define the flow. The main builder uses variable functions to delegate which step it is. The second step, _multistepform_form_food()
does not set a 'step_next'
value and so on submission we don't set a step that will be called when we return to the main builder. I use the 'this_step' value to namespace submitted values in form storage. Other than displaying the message "Complete!" I am not actually doing anything with the final, collected values yet.
The same delegation method using variable functions that the main form builder does can be applied to validate and submit handlers by creating value elements 'step_validate'
and 'step_submit'
with function names as the value in step form builders and the following main validate and submit addition:
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">function </span><span style="color: #0000BB">multistepform_form_validate</span><span style="color: #007700">(</span><span style="color: #0000BB">$form</span><span style="color: #007700">, &</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">) {<br> if (!empty(</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'values'</span><span style="color: #007700">][</span><span style="color: #DD0000">'step_validate'</span><span style="color: #007700">])) {<br> </span><span style="color: #0000BB">$function </span><span style="color: #007700">= </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'values'</span><span style="color: #007700">][</span><span style="color: #DD0000">'step_validate'</span><span style="color: #007700">];<br> </span><span style="color: #0000BB">$function</span><span style="color: #007700">(</span><span style="color: #0000BB">$form</span><span style="color: #007700">, </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">);<br> }<br>}<br></span><span style="color: #0000BB">?></span></span>
And this code in multistepform_form_submit()
.
<span style="color: #000000"><span style="color: #0000BB"><?php<br> </span><span style="color: #007700">if (!empty(</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'values'</span><span style="color: #007700">][</span><span style="color: #DD0000">'step_submit'</span><span style="color: #007700">])) {<br> </span><span style="color: #0000BB">$function </span><span style="color: #007700">= </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'values'</span><span style="color: #007700">][</span><span style="color: #DD0000">'step_submit'</span><span style="color: #007700">];<br> </span><span style="color: #0000BB">$function</span><span style="color: #007700">(</span><span style="color: #0000BB">$form</span><span style="color: #007700">, </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">);<br> }<br></span><span style="color: #0000BB">?></span></span>
Custom validation functions can form_set_error()
normally. By specifying a step submit, our submit function can alter the form flow, skipping steps.
<span style="color: #000000"><span style="color: #0000BB"><?php<br></span><span style="color: #007700">function </span><span style="color: #0000BB">_multistepform_form_like_music</span><span style="color: #007700">(</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'like_music'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'#type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'radios'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#title' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Do you like music?'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'#options' </span><span style="color: #007700">=> array(<br> </span><span style="color: #0000BB">0 </span><span style="color: #007700">=> </span><span style="color: #DD0000">'No'</span><span style="color: #007700">,<br> </span><span style="color: #0000BB">1 </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Yes'</span><span style="color: #007700">,<br> ),<br> </span><span style="color: #DD0000">'#required' </span><span style="color: #007700">=> </span><span style="color: #0000BB">TRUE</span><span style="color: #007700">,<br> );<br> <br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'continue'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'#type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'submit'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#value' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Continue'</span><span style="color: #007700">,<br> );<br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'this_step'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'#type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'value'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#value' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'like_music'</span><span style="color: #007700">,<br> );<br> </span><span style="color: #FF8000">// New value, 'step_submit'.<br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'step_submit'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'#type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'value'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#value' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'_multistepform_form_my_submit'</span><span style="color: #007700">,<br> );<br> </span><span style="color: #0000BB">$form</span><span style="color: #007700">[</span><span style="color: #DD0000">'step_next'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'#type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'value'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'#value' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'_multistepform_form_music'</span><span style="color: #007700">,<br> );<br> return </span><span style="color: #0000BB">$form</span><span style="color: #007700">;<br>}<p>function </p></span><span style="color: #0000BB">_multistepform_form_my_submit</span><span style="color: #007700">(</span><span style="color: #0000BB">$form</span><span style="color: #007700">, &</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">) {<br> if (</span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'values'</span><span style="color: #007700">][</span><span style="color: #DD0000">'like_music'</span><span style="color: #007700">] == </span><span style="color: #DD0000">'0'</span><span style="color: #007700">) {<br> </span><span style="color: #FF8000">// If the user doesn't like music, well don't ask them anything more about it!<br> </span><span style="color: #0000BB">$form_state</span><span style="color: #007700">[</span><span style="color: #DD0000">'values'</span><span style="color: #007700">][</span><span style="color: #DD0000">'step_next'</span><span style="color: #007700">] = </span><span style="color: #DD0000">'_multistepform_form_final'</span><span style="color: #007700">;<br> }<br>}<br></span><span style="color: #0000BB">?></span></span>
In this example if the user says (s)he doesn't like music (Who would choose that, really?) then instead of seeing the step _multistepform_form_music()
(s)he gets sent to _multistepform_form_final()
.
I touched on the 'this_step'
value element earlier, but it plays the part of identifying individual steps, allowing each step to be form alterable. Your implementation of hook_form_alter()
could look for the multi-step form_id and if $form['this_step']
matches what you're looking for. hook_form()
may offer a solution, but I'm not certain it's better or easier than using an additional value element.
After developing this method I was informed it is like Chaos tool suite's wizard. Other multi-step form approaches exist such as Pageroute, an object-based approach to steps of a flow.
The important pieces to use are variable functions and passing the $form_state
array around by reference. For further exploration on this method I plan to see if it could be used to move backwards in a form, returning to previous steps. And the special value elements here are just a convention but I could see them being defined by hook_element()
.
Take a look at the attached .module for a full example spanning more than two steps. The code implements custom validation and a custom submit handler to alter form flow. If you would like to demo the code you'll need a multistepform.info file. Further directions for module development is in the Drupal handbooks.
Update 07/11/2011 The Examples module for Drupal provides multi-step form directions and is a great resource
AttachmentSize multistepform.module_1.txt4.94 KB Tags: DrupalPlanetDrupal