Multipage forms with the Form API (4.7)

Last modified: May 26, 2008 - 02:43

In most cases, web-based forms are one-page affairs: fill in all these fields, click "Submit", and get your result. Naturally, Drupal's Form API excels at making these types of forms. Occasionally, however, there is a need to make a multipage form: a form which stretches across multiple pages and isn't truly "submitted" until the final part is completed. This document addresses an approach to these types of forms. We will assume the following requirements, which are probably more than you actually need, but contain useful tips for style and code structure.

  • Your multipage form has four pages.
  • Your multipage form isn't finished until the "Submit" on page 4.
  • Your multipage form is a node type, and exists in hook_form.
  • Your multipage form hides certain elements on different parts.
  • Your multipage form requires certain elements to be filled in.

Why can't I make multipage forms with what I already know?

Before we begin, the most worrisome question is: why does this document need to exist? To answer that, you'll have to understand the regular workflow of a form in the Drupal Form API. At its simplest:

  • Build
  • Validate
  • Submit
  • Display

The "build" phase is what happens when you code your form - Drupal will take all your form elements and build them internally. Since this built form is often pre-filled with data (in the case of editing, or based on values from $_POST), the next step is "validation", which determines if the values of the form are legitimate for the elements in question (ie., #required fields have values, select #types have values which match an entry in its $options, etc.). Finally, there's the optional "submit" which only happens if the form has been submitted, and the "display" which kicks in when the form is actually being visually rendered in the browser.

This is just dandy for your regular one-page forms.

But, when you consider multipage forms, there comes a time when you need two "build" phases: one for the previous part (that the user has just "submitted") so that it may be properly validated, and another build phase for the current part (that the user is about to view):

  • Build the previous part for validation purposes.
  • Validate the data received from the previous part.
  • Submit (as before and above, again optional).
  • Build the next part if there were no validation errors.
  • Display the next part.

That's where the Form API #pre_render comes in. The #pre_render is an array of function names that you set during your normal "build" phase, and which will be called the split-second before the form is actually displayed to the user. This creates the following workflow, which gives us exactly what we need: the chance to modify our form for the current part after the validation phase for the previous part has finished:

  • Build
  • Validate
  • Submit
  • #pre_render
  • Display

A Fully Working Example

Let's show off some code. As per our feature requirements at the beginning of this document, we're doing a custom multipage node type, so take a look at multipage_form_example_form in the fully functional multipage_form_example.module.

  • Always build your form as if all your form elements were on a single page - this keeps your declarations all in one place, which is stylistically neater. The hiding of the elements on their specific parts will be handled in the #pre_render function.
  • Don't use #required in these declarations either.
  • You can use fieldsets to group your form elements together, but you won't be able to set the #type until the #pre_render function. If you set the fieldset in the initial declarations, you'll have to choose between having the fieldset appear on every page of your multipage (which may be fine for some forms), or not passing the fieldset's child values from part to part (which would generally be a bad idea, unless the fieldset happened to occur on the final part of the form).

Besides #pre_render, which we'll get to shortly, the other important part of our multipage is the powerful hook_form_alter of Drupal's Forms API. Look at multipage_form_example_form_alter: you'll notice that we initialize or, in the case of an existing value, set the current part number that we're on, and modify it as needed depending on if the "Back" or "Preview" has been clicked. Notice that we also modify the #validate and #submit hooks. Instead of just adding our custom functions, we merge with any existing values, which is especially important for custom node types (as we'll need the nodeapi to kick in as normal once our multipage is finally submitted).

The last crucial bit needed for multipages is that #pre_render function we've been talking about - astute readers may have guessed that it is run twice: once in multipage_form_example_form_alter to set the validation requirements for our current/previous page, and then again by Drupal's Forms API, which is when we know that we should increment our counter and show the next form part (as validation has been passed successfully for our previous part).

multipage_form_example_pre_render is the workhorse behind our demo module, and is responsible for setting visibility of elements, whether they are #required, what buttons are available for clicking, and any other visual indication that the user has progressed from page to page. Our logic here is based around using in_array to determine if our current part number is in an array of valid part numbers - which makes it quite simple to allow an element to appear on multiple pages without increasing the size of the code with equality checks.

More information about multipage forms is sprinkled throughout this demo code. Realize, however, that there are still hiccups along the way - 'checkboxes' and multiselect arrays (or any hidden form value that requires array values) are still difficult to pull off. These issues will probably be addressed in a future version of Drupal.

 
 

Drupal is a registered trademark of Dries Buytaert.