Drupal 6: Creating a multipage form using the Forms API

Recently I've been struggling with creating a multi-step CCK node form in Drupal 5. I was able to implement a solution that used a combination of form_alter and jQuery, but due to the amount of jQuery, it was too slow. I then attempted the same functionality in Drupal 6, but due to the number of changes in the Forms API, I decided to take a step back. Here's my first successful attempt at creating a multi-step form using a Drupal 6 module and the Forms API. The following code adds the ability to jump to any step of the form, save at any point, and dynamically creates steps based on the number of fieldsets. My next step will be transposing this code to work with a CCK node form.

Screenshot:

multi step form

<?php

function multipage_menu() {
  $items = array();

  $items['multiStepForm'] = array(
    'title' => t('Multi Step Form'),
    'page callback' => 'drupal_get_form',
    'page arguments' => array('multipage_test_form'),
    'access arguments' => array('access content'),
  );

  return $items;
}

function multipage_test_form(&$form_state) {

  // enable/disable debugging output
  $form_state['storage']['#navigation']['debugging'] = false;

  // per debugging
  if ($form_state['storage']['#navigation']['debugging']) echo "<HR>LOADING FUNCTION: multipage_test_form<HR>";

  // ensure step is set in storage
  if (empty($form_state['storage']['#navigation']['step'])) {
    $form_state['storage']['#navigation']['step'] = 1;
  } elseif ($form_state['storage']['#navigation']['newStep']) {
    // if the form has been submitted, check for a new step
    $form_state['storage']['#navigation']['step'] = $form_state['storage']['#navigation']['newStep'];
    unset ($form_state['storage']['#navigation']['newStep']);
  }

  $form = array();

  // add form fields
  _multipage_test_form_add_fields($form, $form_state);

  // calculate max number of steps
  _multipage_test_form_max_steps($form, $form_state);

  // create tabs
  _multipage_test_form_create_tabs($form, $form_state);

  // remove form fields per step
  _multipage_test_form_remove_fields($form, $form_state);

  // add tabs to form
  _multipage_test_form_add_tabs($form, $form_state);

  // add buttons to form
  _multipage_test_form_add_buttons($form, $form_state);

  return $form;
}

function _multipage_test_form_add_fields(&$form, &$form_state) {

  // per debugging
  if ($form_state['storage']['#navigation']['debugging']) echo "<hr>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ADDING FIELDS<hr>";

  // Fieldset A

  $form['fieldset_a'] = array(
    '#type' => 'fieldset',
    '#title' => t('Fieldset A'),
  );

  $form['fieldset_a']['textfield_a1'] = array(
    '#type' => 'textfield',
    '#title' => t('Textfield A1'),
    '#default_value' => $form_state['storage']['textfield_a1'],
  );

  $form['fieldset_a']['textfield_a2'] = array(
    '#type' => 'textfield',
    '#title' => t('Textfield A2'),
    '#default_value' => $form_state['storage']['textfield_a2'],
  );

  $form['fieldset_a']['textfield_a3'] = array(
    '#type' => 'textfield',
    '#title' => t('Textfield A3'),
    '#default_value' => $form_state['storage']['textfield_a3'],
  );

  // Fieldset B

  $form['fieldset_b'] = array(
    '#type' => 'fieldset',
    '#title' => t('Fieldset B'),
  );

  $form['fieldset_b']['textfield_b1'] = array(
    '#type' => 'textfield',
    '#title' => t('Textfield B1'),
    '#default_value' => $form_state['storage']['textfield_b1'],
  );

  $form['fieldset_b']['textfield_b2'] = array(
    '#type' => 'textfield',
    '#title' => t('Textfield B2'),
    '#default_value' => $form_state['storage']['textfield_b2'],
  );

  $form['fieldset_b']['textfield_b3'] = array(
    '#type' => 'textfield',
    '#title' => t('Textfield B3'),
    '#default_value' => $form_state['storage']['textfield_b3'],
  );

  // Fieldset C

  $form['fieldset_c'] = array(
    '#type' => 'fieldset',
    '#title' => t('Fieldset C'),
  );

  $form['fieldset_c']['textfield_c1'] = array(
    '#type' => 'textfield',
    '#title' => t('Textfield C1'),
    '#default_value' => $form_state['storage']['textfield_c1'],
  );

  $form['fieldset_c']['textfield_c2'] = array(
    '#type' => 'textfield',
    '#title' => t('Textfield C2'),
    '#default_value' => $form_state['storage']['textfield_c2'],
  );

  $form['fieldset_c']['textfield_c3'] = array(
    '#type' => 'textfield',
    '#title' => t('Textfield C3'),
    '#default_value' => $form_state['storage']['textfield_c3'],
  );

  // Fields not in a fieldset

  $form['textfield_d1'] = array(
    '#type' => 'textfield',
    '#title' => t('Textfield D1'),
    '#default_value' => $form_state['storage']['textfield_d1'],
  );

  $form['textfield_d2'] = array(
    '#type' => 'textfield',
    '#title' => t('Textfield D2'),
    '#default_value' => $form_state['storage']['textfield_d2'],
  );

  $form['textfield_d3'] = array(
    '#type' => 'textfield',
    '#title' => t('Textfield D3'),
    '#default_value' => $form_state['storage']['textfield_d3'],
  );

}

function _multipage_test_form_max_steps($form, &$form_state) {

  // calculate max stpes if empty
  if (empty($form_state['storage']['#navigation']['maxSteps'])) {
     $fieldsetCount = 0;
     $otherFields = false;

    // loop through form elements
    foreach ($form as $k => $v) {
      // check for fieldset
      if (substr($k,0,9)=='fieldset_' && is_array($v) && $v['#type']=='fieldset') {
        $fieldsetCount++;
      } else {
        $otherFields = true;
      }
    }

    // if there are other fields, increment max steps
    if ($otherFields) {
      $fieldsetCount++;
      $form_state['storage']['#navigation']['otherFields'] = true;
    }

    // add count to storage
    $form_state['storage']['#navigation']['maxSteps'] = $fieldsetCount;

  }
}

function _multipage_test_form_create_tabs(&$form, &$form_state) {

  // per debugging
  if ($form_state['storage']['#navigation']['debugging']) echo "<hr>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CREATING TABS<hr>";

  // create list of tabs if not set in storage
  if (empty($form_state['storage']['#navigation']['tabs'])) {
    $tabCount = 0;
    $tabs = array();

    // check for other fields
    if ($form_state['storage']['#navigation']['otherFields']) {
      $tabsCount++;
      $tabs[$tabsCount] = "Start";
    }

    // loop through form items
    foreach ($form as $k => $v) {
      if (substr($k,0,9)=='fieldset_' && is_array($v) && $v['#type']=='fieldset') {
        $tabsCount++;
        $tabs[$tabsCount] = $v['#title'];
      }
    }

    // add tabs to storage
    $form_state['storage']['#navigation']['tabs'] = $tabs;

  }

}

function _multipage_test_form_remove_fields(&$form, &$form_state) {

  // per debugging
  if ($form_state['storage']['#navigation']['debugging']) echo "<HR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;REMOVING FIELDS<HR>";

  // set fieldset counter based on otherfields
  if ($form_state['storage']['#navigation']['otherFields']) {
    $fieldsetCount = 1;
  } else {
    $fieldsetCount = 0;
  }

  // loop though form fields
  foreach ($form as $k => $v) {

    // ensure this form item is a fieldset
    if (substr($k,0,9)=='fieldset_' && is_array($v) && $v['#type']=='fieldset') {

      // increment count of fieldsets
      $fieldsetCount++;

      // unset fieldset
      if ($form_state['storage']['#navigation']['step'] != $fieldsetCount) unset($form[$k]);

    } elseif (is_array($v) && $form_state['storage']['#navigation']['otherFields'] && $form_state['storage']['#navigation']['step']>1) {
      // unset field
      unset($form[$k]);
    }

  }

}

function _multipage_test_form_add_tabs(&$form, $form_state) {

  // per debugging
  if ($form_state['storage']['#navigation']['debugging']) echo "<HR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ADDING TABS<HR>";

  $tabsArray = array();
  $tabsCount = 0;

  // loop through tabs and create buttons for each one
  foreach ($form_state['storage']['#navigation']['tabs'] as $k => $v) {
    $tabsCount++;
    $tabsArray['tab_'.$k] = array(
      '#type' => 'submit',
      '#value' => t($v),
      '#attributes' => array(
        'class' => 'tabs' . ($tabsCount == $form_state['storage']['#navigation']['step'] ? ' active' : '')
      ),
    );
  }

  // add tab buttons to beginning of form
  $form = array_merge($tabsArray, $form);

}

function _multipage_test_form_add_buttons(&$form, &$form_state) {

  // per debugging
  if ($form_state['storage']['#navigation']['debugging']) echo "<HR>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ADDING BUTTONS<HR>";

  // add previous button
  if ($form_state['storage']['#navigation']['step']>1) {
    $form['previous'] = array(
      '#type' => 'submit',
      '#value' => t('Previous'),
    );
  }

  // add next button
  if ($form_state['storage']['#navigation']['step']<$form_state['storage']['#navigation']['maxSteps']) {
    $form['next'] = array(
      '#type' => 'submit',
      '#value' => t('Next'),
    );
  }

  // add save button
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Save'),
  );

}

function multipage_test_form_validate($form, &$form_state) {

  // per debugging
  if ($form_state['storage']['#navigation']['debugging']) echo "<hr>FORM VALIDATED<hr>";

}

function multipage_test_form_submit($form, &$form_state) {

  // per debugging
  if ($form_state['storage']['#navigation']['debugging']) echo "<hr>FORM SUBMITTED<hr>";

  // loop through submitted form values
  foreach ($form_state['values'] as $k => $v) {
    // add certain fields to storage
    if (substr($k, 0, 10)=='textfield_') {
      $form_state['storage'][$k] = $v;
    }
  }

  // handle button action
  switch ($form_state['clicked_button']['#value']) {
    case 'Previous':

      // set the state to rebuild
      $form_state['rebuild'] = TRUE;

      // set new step
      if ($form_state['storage']['#navigation']['step']>1) {
        $form_state['storage']['#navigation']['newStep'] = $form_state['storage']['#navigation']['step']-1;
      }

      break;

    case 'Next':

      // set the state to rebuild
      $form_state['rebuild'] = TRUE;

      // set new step
      if ($form_state['storage']['#navigation']['step']<$form_state['storage']['#navigation']['maxSteps']) {
        $form_state['storage']['#navigation']['newStep'] = $form_state['storage']['#navigation']['step']+1;
      }

      break;

    case 'Save':
      // TODO
      break;

    // tabs
    default:

      // check if the button value maps to a tab
      $key = array_search($form_state['clicked_button']['#value'], $form_state['storage']['#navigation']['tabs']);
      if ($key) {
        $form_state['storage']['#navigation']['newStep'] = $key;
      }

      break;
  }

}

?>