Multi-step Node Forms in Drupal 6

Stella Power

on

May 29, 2009

Multi-step Node Forms in Drupal 6

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.
/**
* 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.
/**
* 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.
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.
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.

Share it!

I managed to access the node object from within hook_insert.

There's one bottleneck though. It really need to access $form_state['storage'] but I have no idea how I can access it from within hook_insert. How would you implement that? How do I pass the $form_state variable to hook_insert? Is there a workaround for this?

This post has been very useful to me. Thanks for sharing this, Stella!

In my case I also needed a custom validation handler.

However, I was stuck with the $form_state['storage']['step']

When I implemented my own validation handler like this

<?php mymodule_validation_handler($form, &$form_state) { dsm($form_state} ?>

I get that the storage array is NULL! And the thing is the function does get called!

It turned out that the storage array simply doesn't POST when you define values in the hook_form().

I also learned that the flow is like: validation handler -> submit handler -> hook_form

Lastly, I also need to insert the node into a database. Could you please help me with this? How do I access the $node object? Where and when (last step) do I call hook_insert ?

I'll remembering one thing: Multi-step nodes are apparently a hard thing to do in drupal!

 

 

Will this scale when CCK fields are added/modified in CCK's UI? This seems like an awful lot of code just to make a node form multi-step. There isn't an easier way?
There is a 'multistep' module which is supposed to make a multistep node form from CCK fieldsets. However I couldn't get it to work in Drupal 5 and there's only a dev version available for Drupal 6.