Creating multi-step node forms

Creating multi-step node forms

Posted by stella on Fri, 2009-05-29 21:00 in

Recently I needed a create a multi-step node form in Drupal 6. Unlike other forms in Drupal, it wasn't as simple as configuring a new submit handler that sets $form_state['rebuild'] to TRUE. After trying a few different ways and a bit of searching, I found the solution. The trick is to hide the 'submit' button and use hook_form_alter() on the 'preview' button to regenerate the form for step 2. However, this is probably best explained with some sample code to illustrate.

The first thing you need to do is to define the node form. We're going to use a simple two-step form. On the first page will be the node title and body area, and on the second page a textarea for additional information. Which page of the multistep form to display is determined by $form_state['storage']['step']. As we will see shortly $form_state['storage']['step'] gets set when the first page of the form is submitted.

<?php
/**
* Implement hook_form().
*/
function multistep_form(&$node, $form_state) {

 
// Initial step: display title and body fields.
 
if (!isset($form_state['storage']['step'])) {
   
$form['title'] = array(
     
'#title' => t('Title'),
     
'#type' => 'textfield',
     
'#required' => TRUE,
     
'#default_value' => isset($form_state['storage']['title']) ? $form_state['storage']['title'] : $node->title,
    );
   
$form['body_field'] = node_body_field($node, t('Body'), 1);
  }
 
// Second step: display
 
else {
   
$form['additional_info'] = array(
     
'#title' => t('Additional information'),
     
'#type' => 'textarea',
     
'#required' => TRUE,
     
'#default_value' => isset($form_state['storage']['additional_info']) ? $form_state['storage']['additional_info'] : $node->additional_info,
    );
  }

  return
$form;
}
?>

Using hook_form_alter() we can change the first page of the form and make it into a multistep form. We can identify the first step of the form by $form_state['storage'][step'] and so only call multistep_make_node_multistep()for that step. This prevents us from hiding the submit button on the final page.

<?php
/**
* Implement hook_form_alter().
*/
function multistep_form_alter(&$form, $form_state, $form_id) {
  if (
$form_id == 'multistep_node_form') {
   
$node = $form['#node'];

   
// Page 1, $form_state['storage']['step'] isn't set yet, so display first form.
   
if (empty($form_state['storage']['step'])) {
     
// Hide everything except title, body and button fields.
     
$fields = array('title', 'body');
     
multistep_make_node_multistep($form, $fields, 'multistep_step1_form_next_handler');
    }
  }
}
?>

Originally I used a form id specific form alter function but these form alter functions are invoked before the general case form alter functions. This means that other modules, e.g. menu, that modify the node form do so after the unwanted fields are hidden, regardless of the modules relative weights. The only way to hide the fields added by these other modules is to use the general hook_form_alter() function rather than the form id specific one.

The multistep_make_node_multistep() function takes in an array of fields that should not be hidden, along with the name of the submit function to use with the 'preview' button. Any field that doesn't appear in the array, other than hidden fields and a few other special ones, are prevented from being displayed by setting #access to FALSE.

<?php
function multistep_make_node_multistep(&$form, $fields, $submit_handler) {
 
// Hide all the elements we don't want.
 
foreach (element_children($form) as $child) {
    if (
$child != 'buttons' && !in_array($child, $fields) &&
        (empty(
$form[$child]['#type']) ||
         (
$form[$child]['#type'] != 'hidden'
         
&& $form[$child]['#type'] != 'value'
         
&& $form[$child]['#type'] != 'token'))) {
     
$form[$child]['#access'] = FALSE;
    }
  }

 
// Hide the submit button.
 
$form['buttons']['submit']['#access'] = FALSE;

 
// Change the 'preview' button to 'Next' and set the submit handler.
 
$form['buttons']['preview'] = array(
   
'#type' => 'submit',
   
'#value' => t('Next'),
   
'#weight' => 50,
   
'#submit' => array($submit_handler),
  );
}
?>

Finally we configure the submit function for the 'Next' button on the first page of the form. When the button is clicked, this function will be called, setting $form_state['storage']['step'] to 1.

<?php
function multistep_step1_form_next_handler($form, &$form_state) {
 
$form_state['storage']['step'] = 1;
}
?>

Using this method, and by using multiple submit functions which increment the value of 'step' each time the 'Next' button was clicked, I was able to make a custom node form into a 4 step form!

Credit goes to dww whose post at http://drupal.org/node/382634#comment-1306916 showed me why using the form id specific hook_form_alter() wasn't hiding all form fields. dww also provides a sample module which implements a simple two step node form which you can download and try out.

Nice

This is far more elegant than the approach I took once which involved unsetting various fields based on URL arguments.

Posted by dalin (not verified) on Mon, 2009-06-01 07:43
For the hook_form() to be

For the hook_form() to be triggered I think you need to implement hook_node_info().
How will you create a wizard for a node's module node?

btw, in if (empty($form_state['storage'][step'])) { you have a missing '.

Posted by Amitaibu (not verified) on Sun, 2009-06-07 10:14
Yep

You need to define a hook_node_info() for each content type too.

Posted by stella on Sun, 2009-06-07 10:20
Very Instructive

Thanks for taking the time to share this, which is one of the best examples I have found on creating multi-page forms.

Posted by John H (not verified) on Sat, 2009-07-04 01:47
#access attribute

I was seeing a runtime error in form.inc related to setting the #access attribute in multistep_make_node_multistep(). I opted to instead unset the array element, which solved the problem. Change:

$form[$child]['#access'] = FALSE;

to

unset($form[$child]);

Posted by John H (not verified) on Sun, 2009-07-05 21:51
CCK forms?

any clue regarding how to apply the above feature to a pre-built cck content-type form? (except the multi-step module)

-jayesh

Posted by jayesh (not verified) on Wed, 2009-09-23 10:50
CCK Content Type

Asking the same question pre-built cck content-type form as well

Posted by oyunlar (not verified) on Wed, 2009-10-07 20:57
Hi Stella, I'm not quite

Hi Stella,

I'm not quite sure about the $form_state['storage']['title']. I'm implementing a previous button and want to re-populate my previous fields when the user goes back. I can store the $form_state['storage']['step'] no issue there, but this is the only value stored in the array 'storage'. Meaning, I cannot retrieve $form_state['storage']['title'] for example like your code says so.

Thanks,
JG

Posted by Jose G (not verified) on Thu, 2010-01-07 16:16
Forgot to mention, I see the

Forgot to mention, I see the values I'm looking for in $form_state['values'], so I'll try going down this road and see what happens :-)

JG

Posted by Jose G (not verified) on Thu, 2010-01-07 16:17
for steps greater than 2

IIRC, once you go past the 2nd step in a multi-step form, the values submitted in the first form page are no longer available in $form_state['values'], unless of course you add them as hidden fields. Any information you want to keep, you need to add to $form_state['storage']

Posted by stella on Thu, 2010-01-07 16:22
Thanks Stella, I reviewed

Thanks Stella, I reviewed http://drupal.org/node/382634#comment-1306916 there is manual storage of the values in ['storage'] during the processing, and the one difference between your code and his is that all the fields are provided at once during hook_form(), customized during hook_form_alter() and stored in ['storage'] during the different steps. It was just my missing piece.

Thank you for all the help though, it's been very helpful, I was ready to start playing with batch_set().

JG

Posted by Jose G (not verified) on Thu, 2010-01-07 21:27
Very Instructive

Thanks for taking the time to share this, which is one of the best examples I have found on creating multi-page forms.

Posted by Giochi Di Ben 10 (not verified) on Thu, 2010-01-21 21:07
good

I have found on creating multi-page forms.Thanks for sharing.

Posted by goldpreis (not verified) on Fri, 2010-03-05 12:51
Thank you very much for the

Thank you very much for the excellent and useful subject.

Posted by alisveris (not verified) on Tue, 2010-03-23 10:04
MultiStage inside Lightbox/dialogBox/ModalBox

Hey,

I'm new to drupal and want to achieve the same but inside the modalBox/DialogBox or inside popup using jquery. I tried hands on "Dialog", "lightbox" drupal projects and want to achieve multi stage user login inside the dialog or modal box using jquery.

Would appreciate your help or suggestions in the same regard.
Thanks

Posted by Akshay (not verified) on Tue, 2010-10-19 19:18
lightframe

Why not use lightbox2 with rel="lightframe" to load the page? It should be two separate things. Get it working without the lightbox first, and then load the page in a lightframe. However, the lightframe won't close automatically on the last form page submission.

Posted by stella on Tue, 2010-10-19 22:03
thanks for the nice work! i

thanks for the nice work!

i really miss the comments on what info to change to make it work for my content type. it maybe just me, but something like YOURCONTENTTYPE_node_form would really make stuff easier to understand.

is "multistep" the name of the module _and_ the content type _and_ the topic itself? this makes it very confusing, at least for me ;)

anyway, thanks a lot!

Posted by bongo (not verified) on Sat, 2010-11-13 14:02
Thanks for your post!

It exactly I needed for developing my Drupal module.

Posted by aliciagh (not verified) on Tue, 2010-11-30 20:30
Why hide the fields, and why change 'Preview'?

Hello,

I'm a bit confused here... why was it required to hide all those fields? Or it isn't exactly *required*, but rather desired, to keep the form clean?

Is there a reason for turning the 'Preview' button into 'Next'? Why wasn't it unset(), and a new one created, with the ID 'next'?

Cheers!

Posted by Camil B (not verified) on Fri, 2010-12-03 15:55
thanks for your tips ,but I

thanks for your tips ,but I am still a little confused about the codes ,so may i contact you and give me an instant reply

Posted by Anonymous on Mon, 2012-01-02 04:44
The reason why we need the

The reason why we need the obd2 auto diagnostic tools is the need for curbing "mobile emissions". In tune with the need for global preservation, the EPA has been empowered to place firm emission standards. This has compelled the car manufacturers to bring in better non-polluting cars into the market. In turn, such strict measures for controlling emission levels have also helped to increase the functional life of the vehicle as well.

Posted by auto diagnostic tool (not verified) on Wed, 2012-01-11 08:15