GVS is now part of Acquia. Acquia logo

Drupal 7 multistep forms using variable functions

Ben's picture

I like building forms. So much so that I've even been teased about it. Despite that I want to share how multistep forms have changed for Drupal 7 and to expand on how you can use variable functions to achieve cleaner and easier form step logic, including easily moving backwards in forms. Understanding multistep in Drupal 7 was prompted by my need to create easy forms for an internal GVS project that will hopefully launch soon.

Multistep in Drupal 7

In Drupal 6 to carry data back to your form builder you set the storage key of $form_state in your submit handler. In Drupal 7, upon return to your builder after submission, you carry data over by keeping the Form API from pulling the form array out of cache*. You do so by setting $form_state['rebuild'] to TRUE in your validate or submit handlers. Another change is the first argument of your builder must be $form because of changes to drupal_get_form(). &$form_state is now your second argument to your form builder.

Update: 'rebuild' existed in Drupal 6 (thanks Wim) but now seems to be required for multistep to work in Drupal 7.

Drupal 7:

<?php
// Form builder definition.
function my_form($form, &$form_state) {...}

// Form submit handler.
function my_form_builder_submit($form, &$form_state) {
 
// Trigger multistep.
 
$form_state['rebuild'] = TRUE;
 
// Store values that will be available when we return to the definition.
 
$form_state['storage']['values'] = $values;
}
?>

Let's look at a example:

<?php
// multistep_simple, our form builder function.
function multistep_simple($form, &$form_state) {
 
// Check if storage contains a value. A value is set only after the form is submitted and 'rebuild' is set to TRUE.
 
if (!empty($form_state['storage']['myvalue'])) {
   
// Display a message with the submitted value.
   
drupal_set_message(t("You submitted: @name", array('@name' => $form_state['storage']['myvalue'])));
  }
 
$form['name'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Name'),
   
'#description' => t('Enter your name'),
   
'#required' => TRUE,
  );
 
$form['submit'] = array(
   
'#type' => 'submit',
   
'#value' => t('Submit'),
  );
  return
$form;
}

// Our submit handler for multistep_simple.
function multistep_simple_submit($form, &$form_state) {
 
// Tell FAPI to rebuild.
 
$form_state['rebuild'] = TRUE;
 
// Store submitted value.
 
$form_state['storage']['myvalue'] = $form_state['values']['name'];
}
?>

We could of course set the message in the submit handler but I hope the example helps convey the two points of the definition arguments and setting rebuild in the submit handler.

Multistep with variable functions and FAPI handler convention

Using variable functions is a great way to make readable form step logic. And, if you combine this method with Drupal's form handler definition style and you can easily make advanced forms.

Update: This is by no means a method that exists only in Drupal 7. This is just a process I've written about before for making form-step logic easier to code and extend. What I'm writing about here is an expansion on that process.

The components:

  1. Store step names (function definitions) in $form_state['storage']
  2. Return the current step's form array in the main FAPI builder
  3. Invoke validate and submit handlers for a individual step by appending '_validate' or '_submit' to the step definition

Let me start with the form builder and submit handler for illustration. The form ID known to FAPI will be multistep_form and multistep_form_submit the submit handler. The builder uses functions defined in storage to know which step to return and the submit handler uses FAPI convention to call a individual step's validate or submit.

<?php
// Primary form builder.
function multistep_form($form, &$form_state) {
 
// Initialize.
 
if ($form_state['rebuild']) {
   
// Don't hang on to submitted data in form state input.
   
$form_state['input'] = array();
  }
  if (empty(
$form_state['storage'])) {
   
// No step has been set so start with the first.
   
$form_state['storage'] = array(
     
'step' => 'multistep_form_start',
    );
  }

 
// Return the form for the current step.
 
$function = $form_state['storage']['step'];
 
$form = $function($form, $form_state);
  return
$form;
}

// Primary submit handler.
function multistep_form_submit($form, &$form_state) {
 
$values = $form_state['values'];
 
// Check if we're moving back or forward in the form.
 
if (isset($values['back']) && $values['op'] == $values['back']) {
   
// Code for moving in reverse left out for now...
 
}
  else {
   
// Record the current step.
   
$step = $form_state['storage']['step'];
   
$form_state['storage']['steps'][] = $step;
   
// Call step submit handler if it exists.
   
if (function_exists($step . '_submit')) {
     
$function = $step . '_submit';
     
// Current step's submit handler will set the next step.
     
$function($form, $form_state);
    }
  }
  return;
}
?>

Step submit handlers specify the next step to use and step builders can skip steps.

Here's a diagram depicting the flow of standard FAPI and how the builder and handlers call off to a individual step.

Here's an example individual step and it's submit handler:

<?php
function multistep_form_start($form, &$form_state) {
 
$form['musician'] = array(
   
'#type' => 'radios',
   
'#title' => t('Choose a musician'),
   
'#options' => array(
     
'davis' => t('Miles Davis'),
     
'coltrane' => t('John Coltrane'),
     
'corea' => t('Chick Corea'),
     
'brubeck' => t('Dave Brubeck'),
     
'other' => t('Other')
    ),
   
'#default_value' => isset($form_state['storage']['musician']) ? $form_state['storage']['musician'] : NULL,
  );
 
// Next button.
 
$form['submit'] = array(
   
'#type' => 'submit',
   
'#value' => t('Next'),
  );
  return
$form;
}

function
multistep_form_start_submit($form, &$form_state) {
 
// Trigger multistep, there are more steps.
 
$form_state['rebuild'] = TRUE;
 
$values = $form_state['values'];
  if (isset(
$values['back']) && $values['op'] == $values['back']) {
   
// User is moving back from this form, clear our storage.
    // [...]
 
}
  else if (
$form_state['values']['musician'] == 'other') {
   
// User chose 'other' so inject an intermediary step.
   
$form_state['storage']['musician'] = NULL; // Clear out because of our define musician step uses key 'musician' as well.
   
$form_state['storage']['step'] = 'multistep_form_define_musician';
  }
  else {
   
// We could do something with the values here, like saving etc...
    // Hold on to value.
   
$form_state['storage']['musician'] = $form_state['values']['musician'];
   
// Set the next step.
   
$form_state['storage']['step'] = 'multistep_form_songs';
  }
}
?>

The above example defines a single step and sets us up to move through the chain of several. Let's look at the next step and define how we could move backwards. We'll redefine the primary submit handler now.

Update: The form property #limit_validation_errors and a element submit property are required on back buttons should any step have anything that would trigger validation.

<?php
// Primary submit handler.
function multistep_form_submit($form, &$form_state) {
 
$values = $form_state['values'];
  if (isset(
$values['back']) && $values['op'] == $values['back']) {
   
// Moving back in form.
   
$step = $form_state['storage']['step'];
   
// Call current step submit handler if it exists to unset step form data.
   
if (function_exists($step . '_submit')) {
     
$function = $step . '_submit';
     
$function($form, $form_state);
    }
   
// Remove the last saved step so we use it next.
   
$last_step = array_pop($form_state['storage']['steps']);
   
$form_state['storage']['step'] = $last_step;
  }
  else {
   
// Record step.
   
$step = $form_state['storage']['step'];
   
$form_state['storage']['steps'][] = $step;
   
// Call step submit handler if it exists.
   
if (function_exists($step . '_submit')) {
     
$function = $step . '_submit';
     
$function($form, $form_state);
    }
  }
  return;
}

function
multistep_form_songs($form, &$form_state) {
 
$form['song'] = array(
   
'#type' => 'textfield',
   
'#title' => t('Favorite recording?'),
   
'#default_value' => isset($form_state['storage']['song']) ? $form_state['storage']['song'] : NULL,
  );
 
$form['unknown'] = array(
   
'#type' => 'checkbox',
   
'#title' => t("I don't know"),
  );
 
$form['back'] = array(
   
'#type' => 'submit',
   
'#value' => t('Back'),
  );
 
$form['submit'] = array(
   
'#type' => 'submit',
   
'#value' => t('Next'),
  );
 
  return
$form;
}

function
multistep_form_songs_submit($form, &$form_state) {
 
$values = $form_state['values'];
 
$form_state['rebuild'] = TRUE;
  if (isset(
$values['back']) && $values['op'] == $values['back']) {
   
// User is moving back from this form, clear our storage.
   
$form_state['storage']['song'] = NULL;
  }
  else if (
$values['unknown']) {
   
// Skip to confirm step.
   
$form_state['storage']['step'] = 'multistep_form_confirm';
  }
  else {
   
$form_state['storage']['song'] = $form_state['values']['song'];
   
// Set the next step.
   
$form_state['storage']['step'] = 'multistep_form_heard';
  }
}
?>

In our second step form builder (multistep_form_songs) we provide a back button. The primary submit handler allows the current step to remove stored data (to avoid a previously set default value when we return) and then removes the most recent step so when returned to the primary builder the previous step is used.

A full multistep example is provided in the attachment. You'll need to rename the files excluding the '.txt' extension to use. Some assumptions are made, of course, so adjustment for your use cases is required.

P.S.
If you didn't catch it, in Drupal 7 you are not limited to 'storage' for carrying data across requests. It's used in most of the examples, but you'll see that it's not required.
*I'm not clear on some core implementation specifics regarding the form cache so if you know please share!

AttachmentSize
multistep.module.txt9.85 KB
multistep.info_.txt94 bytes

Comments

i like your variable-function

i like your variable-function technique and would liken it to a state machine; in some cases -- for example when you'd want to more easily insert or delete steps in the multi-step without altering code -- you could even tableize the steps and make it a table-driven state machine

I don't see anything that's

I don't see anything that's only possible in Drupal 7. All of this is exactly the same in Drupal 6, with the exception of the function signature of the form builder function.

The multistep method of using

The multistep method of using variable functions is certainly achievable in Drupal 6, I've written about it before. But, what is different in Drupal 7 is setting rebuild to TRUE, along with the function signature changes. And, you don't explicitly have to use 'storage'.

$form_state['rebuild'] = TRUE

$form_state['rebuild'] = TRUE also exists in Drupal 6.

'storage' was indeed the only supported "form state storage namespace" in Drupal 6, but that's extremely minor.

My point is that this article is titled "Drupal 7 multistep forms", indicating that this is hard to do in 6 but easy in 7, and that's just not true.

Thanks for your

Thanks for your clarification, Wim. I by no means meant to imply that this method is easy to do in 7 and not 6.

This is totally not how you

This is totally not how you do a multistep in Drupal 7. By nuking $form_state['input'] = array(); you revert to Drupal 6, more or less. But even with those, like the node form, http://drupal4hu.com/node/246 is an easier way (a lot) using, indeed Drupal 7 features.

Not finding a lot of

Not finding a lot of documentation at the time I looked at the core tests for FAPI to understand multistep so my approach may not be ideal and I must say I don't understand the internals. Can you elaborate on what you mean by nuking $form_state['input'] being wrong? And from your post we are the same on everything but clearing input and avoiding validation errors.

So my example was for node

So my example was for node forms. Instead of bothering with building a different form for different steps, you build one form and then apply #access FALSE to the pieces you don't want to see. The only problem with this approach in Drupal 6 is that the elements in future steps will throw validation errors at you so you use the new #limit_validation_errors on your Next button to avoid this problem.

Node forms, similarly to your approach does not use the form_state['input'] persistence. That's required for some AJAX magic I am not yet too familiar with either. I will ask rfay and effulgentsia to chime in. And yeah we need to write more on this subject.

From Randy Fay: " The

From Randy Fay:
"

The Examples Module also now has a multistep example, among others.

In addition, the AJAX Example there has an AJAX multistep form that degrades to regular multistep.

Neither of these is ambitious as this form, but your review and comments would be appreciated.

Two final things:
1. If any elements are validated and you have a "back button" you may need to use #limit_validation_errors to indicate that no validation is required if you're just going back.

2. $form_state['storage'] no longer has any special significance. You could use $form_state['step1'] or whatever and it works the same.

3. The field on this comment form for "email address" has no title.

"

Great Example

Great example of using the default node form structure to do multistep!

I'm trying to work out if it can be AJAX-ed so that only the form is loaded with each step rather than the entire page? Might this be possible or do I need to build such a form outside of the core node form?

GVS projects

CertifiedToRock.com was created to allow community members and employers to get a sense of someone's involvement with the Drupal project.

GVS is now part of Acquia.

Acquia logo

Contact Acquia if you are interested in a Drupal Support or help with any products GVS offered such as the Conference Organizing Distribution (COD).

We Wrote the Book On Drupal Security:

Cracking Drupal Book Cover