header photo

via positiva

Dynamic and Multi-Step With Drupal's Form API

The release of Drupal 4.7 introduced the Form API, a framework for building, displaying, validating, and processing HTML forms. With it, forms are defined as structured arrays, and those structured arrays contain all the information necessary to properly handle the form throughout its life cycle. This approach also makes it possible for modules to customize other forms (adding additional fields to a signup page, for example), and allows designers to customize the on-screen display of forms using overridable theming functions. It also makes validating form input, and avoiding form tampering, much easier. That's great!

The tradeoff of those enhancements was the loss of flexibility in certain complex scenerios -- in particular dynamic forms that change based on user input, and multi-step 'wizard' style forms. With the introduction of Form API 2.0 in version 4.8/5.0 of Drupal, though, we've eliminated the limitations that made those cases so difficult.

Dynamic Forms: Three Different Scenerios

For the purposes of this article, we'll be looking at three specific kinds of dynamic forms:

  1. The Long Form, a large form divided into multiple steps for ease of use
  2. The Wizard, a form where a user's answers in each step determine the options available in the next
  3. The Form That Builds Itself, a page where some options (clicking 'add more choices' when setting up a poll, for example) add more fields until the user chooses to submit the 'finished product.'

While the first scenerio isn't really dynamic (it simply presents different subsets of options to the user at each step along the way), all of these scenerios work by changing the form depending on the data that the user has just submitted. And that means that all three scenerios encounter the same options in the current Form API. Why is that? Read on...

Form API 1.0

Now that we have a handle on the different kinds of forms we're dealing with, let's take a look at the current state of affairs with Drupal's form-building. It will help us understand where the problem lies.

  1. Your page-building function is called.
  2. It builds a form definition array.
  3. It calls drupal_get_form($form_id, $form), passing in a unique ID for the form and the form's definition array.
  4. drupal_get_form() first checks to see if there is incoming POST data. If there is, and the form_id in that POST data matches the $form_id you passed in, it knows that the user is submitting the form and begins the 'processing' stage.
    1. In the processing stage, drupal_get_form() passes the form array to drupal_validate_form(). Any validation handlers added to specific fields are processed there.
    2. If the validation stage completes and no errors are found, drupal_get_form() hands the form off to drupal_submit_form(). In that stage, data is processed, database changes are made, and so on.
    3. drupal_submit_form() can return a URL to redirect the user to when form processing is done. If it does, drupal_get_form() redirects to that page and the Form API is finished.
  5. If there is no incoming POST data. the processing stage returned validation errors, or no redirect URL was returned by drupal_submit_form(), the form array is passed to drupal_render_form() and the actual HTML is generated.
  6. Your page building function receives the generated HTML from drupal_get_form(), complete with hilighted errors if appropriate, and displays it to the user.
  7. If the user enters in values (either filling them in for the first time, or correcting errors) and clicks the Submit button, the form submits to itself, triggering the page building function in step 1... And the cycle repeats.

The Problem With Dynamic Forms

That system works well for static forms: build the array, check for incoming POST values, validate, submit, and optionally render for display. Since a Form API form always submits to itself, the same definition array is used when first displaying the form, and validating it. Unless, that is, you need to display forms that change...

In that case, you run into the following problem:

  1. Your page building function generates the 'starting point' for your dynamic form.
  2. drupal_get_form() renders it, and displays it.
  3. The user enters their choices and clicks submit.
  4. Your page building function looks at the incoming POST values, and creates a form with new or additional options based on the user's choices.
  5. Your page building function hands the form off to drupal_get_form()... but the fields it contains no longer match the values coming in from the user! Validation functions throw errors, and everything grinds to a halt.

What we really need to do is build two form definition arrays. The first should be a duplicate of the form from step 1, to use during the processing of incoming form values. The second should be the 'new' form from step 4, to display to the user if the first one passes validation.

Form API 2.0: The Solution


In Drupal 4.8/5.0, the freshly retooled Form API adds just that: the ability to process incoming values with one form array, while displaying a new form array for user input. Three changes make this possible.

  1. Forms are now built by dedicated functions, and only the form's ID needs to be submitted to drupal_get_form().
  2. These form building functions can accept parameters, like the node object to be edited or a collection of values representing the user's submitted data.
  3. When a form submits to itself, drupal_get_form() can save the parameters used to build the form array. The next time it submits, it will re-use the stored parameters to recreate the first form for processing, then display the newly built form to the user.

The third item in that list is one of the most important: all you need to do is create your form building function, and drupal_get_form() will handle pulling up the right version of the form (one for processing, one for display) during each phase. Because this behavior is only necessary for some forms, drupal_get_form() only stores that information if the #multistep property is set to TRUE in your form definition array.

How to Handle the Three Scenerios

Let's step back for a moment. We now understand the cause of the problem in Form API 1.0: a mismatch between the user's submitted input, and the form array that's build based on it. We also understand how Form API 2.0 fixes that, allowing one form array to be used for processing and another for display. With that in mind, how can your module handle the three dynamic form scenerios outlined at the beginning of the article?

The Long Form

As with all of these scenerios, the real action will happen in your form building function. It will use hidden fields to indicate what 'stage' is currently being displayed, and to store the user input from previous stages. While this was theoretically possible using Form API 1.0, the new features make it much simpler. Here's an example of how your form-building code would look:

function my_form($param1, $param2, $form_values = NULL) {
// In a multistep form, drupal_get_form() will always
// pass the incoming form values to you after any other
// parameters that you specify manually. Do this instead
// of looking at the incoming $_POST variable manually.

if (!isset($form_values)) {
$step = 1;
}
else {
$step = $form_values['step'] + 1;
}

$form['step'] = array(
'#type' => 'hidden',
'#value' => $step,
);

switch ($step) {
case 1:
// Create the fields for the first step of your form here
break;

case 2:
// First, add a hidden field for each of the incoming
// form values.
// Then, add the fields regular form fields that the user
// will see in this second step.
break;

case 3:
// And so on and so forth, until you've reached the final
// step.
break;
}

// This part is important!
$form['#multistep'] = TRUE;
$form['#redirect'] = FALSE;

$form['submit'] = array(
'#type' => 'submit',
);

return $form;
}

In the validation code for your form, you can check the 'step' field to see which set of fields you need to check, and display any errors. The function will keep accumulating hidden values and displaying a new set of fields until it reaches the final step. You'll probably want to prevent your form submission handler from processing the form data until all the steps have been completed. To do that, something like this would be effective:

function my_form_submit($form_id, $form_values) {
$final_step = 10;

if ($form_values['step'] == $final_step) {
// Process the form here!
}
}

The Wizard

This scenerio, from a code perspective, is almost exactly the same as The Long Form. Depending on how your Wizard works, though, you may want to have your form submission handler actually process each step's data, rather than storing it as hidden fields in the next step. Or, you may want to process 'batches' of steps together before proceeding. For example:

function my_form_submit($form_id, $form_values) {
switch ($form_values['step']) {
case 3:
// Process the form data from steps 1, 2, and 3
break;
case 9:
// Process the form data from steps 4 through 9
break;
case 10:
// Process the form data from step 10,
break;
}
}

For very complex wizards, you may also want to split out individual steps as helper functions, but you'll still need to use the 'core' function as the central dispatcher. It's the one that the Form API knows to call automatically during the building and processing stages.

The Form That Builds Itself

The final scenerio is a bit trickier. Rather than dividing the form submission process into discrete steps, it involves a user making choices that continue to change the form (by adding new fields, in most cases) until they're happy with the results. How would we handle it?

In the example below, our hypothetical module is displaying a form that allows a user to create a quiz. Some fields, like the title and description of the quiz, will always be present. The first three slots for 'quiz questions' will always be available, too. But if the user selects the 'Give me more questions' checkbox, it will build the form with five more boxes.

function my_form($param1, $param2, $form_values = NULL) {
// Build the fields that stay the same from form to form...
// And populate the default_values for each field with the
// corresponding entry from $form_values
$form['title'] = array(
'#type' => 'textfield',
'#title' => t('Quiz title'),
'#default_value' => isset($form_values) ? $form_values['title'] : '',
);

// The current number of questions, plus three more if
// the user requested them.
if (isset($form_values)) {
$question_count = $form_values['question_count'];

if ($form_values['more_questions'] == 1) {
$question_count = $question_count + 3;
}
}
else {
$question_count = 3;
}

$form['question_count'] = array(
'#type' => 'hidden',
'#value' => $question_count,
);

// We'll loop from 1 to n, where n is the current number of questions to
// be displayed. We'll automatically populate each question with any data
// that was entered in the previous trip through the form.
for ($i = 1; $i <= $question_count; $i++) {
$form['question_' . $i] = array(
'#type' => 'textfield',
'#title' => t('Question !count', array('!count' => $i)),
'#default_value' => isset($form_values) ? $form_values['question_' . $i] : '',
);
}

$form['more_questions'] = array(
'#type' => 'checkbox',
'#title' => t('Give me more questions'),
'#return_value' => 1,
);

// This part is important!
$form['#multistep'] = TRUE;
$form['#redirect'] = FALSE;

$form['submit'] = array(
'#type' => 'submit',
);
}

function myform_submit($form_id, $form_values) {
if ($form_values['more_questions'] == 1) {
// Don't process the form. We're rebuilding it with more question fields.
}
else {
// Loop through all of the questions
for ($i = 1; $i <= $form_values['question_count']; $i++) {
$current_question = $form_values['question_' . $i];
// Process $current_question
}
}
}

The above code is a bit more complicated than the previous scenerios, but what it's trying to accomplish is pretty simple. When it first displays, it presents the user with a Title field and three empty question fields. It also has a hidden field storing the current number of questions, and a checkbox indicating that the user wants more options.

The form submission function checks to make sure that the user isn't in the process of adding more options before trying to process anything. When they finally submit without that checkbox selected, it loops through the list of question fields and saves each one individually.

The Wrapup

What have we learned? With the new features in Form API 2.0, it's possible to create forms that change each time a user submits them, validating as they go, and processing the results at arbitrary points along the way. There are different approaches to this task, depending on what kind of user interface your form requires, but most depend on storing information about the form in hidden fields, and using it to control how the next step is built.

Large, complex forms can lead to large, complex code. If you run into challenges, or can't get something working, remember to break it down into pieces and trace your way through the Form API workflow. Make sure you're doing the right thing at the right time, and that the values you expect to be getting in your builder function are arriving properly.

Happy coding!

Awesome article

Very useful - FAPI 2.0 is going to rock the known universe!

Thanks!

#submit should return false until the form is complete...

I didn't really see it in the examples so i thought I'd mention it here after I spent a while cursing a form... :) Maybe I can't read.

In addition...

If you use '#button' rather than '#submit' as your input type, it will do the roundtrip and fire validation hooks, but WON'T fire submit hooks... That's probably the easiest way to avoid accidental triggering.

A little HTML for your sig

I &heart; Drupal. Works if it's parsed as HTML.

Incomplete examples.

When I tried to write a multistep form to learn how to use them, I found that I needed to set $form['#redirect'] = FALSE. Without it, $form_values was always empty.

I made sure I was using the form.inc in HEAD, but I didn't know about this flag needing to be set until I saw someone's problem with 4.7 [[http://drupal.org/node/56129|here]].

Use session array instead of hidden fields

Hey,
shouldn't it be easily possible to store the values of the previous step in the session array instead of hidden fields?

So you would use your submit function to store $form_values in $_SESSION['my_form'], and in your my_form(..) you'd then read our $_SESSION['my_form'] if it's a wizard style form.

This would reduce html overhead, and maybe there are situations (e.g. forms with long text areas or with heaps of steps) where you don't want to transfer all the data of all the previous steps each time again up to the last step.

regards,
frando

frando, fields of type =>

frando, fields of type => '#hidden' don't get rendered to the client.

oops

actually, 'type' =>'#value' fields aren't rendered but #hidden fields are.

Jeff is likely using #hidden because #value fields don't seem to get passed with $form_values into the validate function for #multipart forms.

Is this a bug?

Nope...

type=>value fields are intended to work that way. They let you hang data off of a form so that submit and validate functions, or hook_form_alter implementations, can use them without polluting the HTML...

What about 'back' buttons?

I'm working on the "Long Form".

I had already done it with Drupal 4.7, #pre_render and the multiple page approach but the 5.0 way looks much more elegant and simpler.

I was wondering, what about 'back' buttons? How can I revert from step 3 to step 2 and from 2 to 1 "refilling" the form?

Thank you so much for this great information and clear explanation Jeff.

Cheers!

Awesome!

WD Jeff, you've hit the nail on the head and helped me a ****load. Now I feel like I'm getting my head around the forms API. Some good comments here too, like the #button rather than submit... works a charm.
I'm developing a dependency based selection wizard (hopefully generic enough to submit to drupal), so just reading this has helped so much. The tricky part is that I need to store data that the user never sees... then I read the comments and see $_SESSION - thank you frando!!! wood & trees thing I guess :)

I came here looking for

I came here looking for something slightly different, but this is a clear and useful writeup, and the comment about #button answers a question I didn't know I had.

MultiStep, more like 2 step...

Try adding a required feild into step two, submit the form while it is empty. Then re-submit the form with a value in the required field , and it reverts back to step one, instead of going to step three. Although as much as I like drupal, it still has a long way for development. Especially when it comes to simple tasks like that.

File a bug.

Go to drupal.org, file a bug report, and we'll take a look. :-) I'm away from home this week working on a training conference, but if what you're describing is a verifiable bug, it can be fixed and included in the next minor update of Drupal 5, along with other bug fixes.

Since a very small number of people have utilized the multistep form APIs, it's not shocking that a few issues have been discovered. :-) You can be part of the solution.

Can't get to my _submit function

Hi Jeff,

I am a newbie to Drupal, and recently upgraded to Drupal 5 for the multistep functionality. I currently used your example to create and render the multistep form, but for some reason, I can't get out of my_form function? Is there a way to change my callback to get me to my submit function with my $form_values?

I hope this makes sense.

Let me know if you need more information. ;) Thanks

Hmmm...

I am a newbie to Drupal, and recently upgraded to Drupal 5 for the multistep functionality. I currently used your example to create and render the multistep form, but for some reason, I can't get out of my_form function? Is there a way to change my callback to get me to my submit function with my $form_values?

Hello! This is actually something that's a bit confusing about the API, that we're hoping to improve over the next version. the form id of your form (aka, the name of the function that builds the form) is what's usually used to find the submit handler. In other words, if your form is built by 'my_form_function', then the submit handler is 'my_form_function_submit'.

Does that make any sense?

Is it posible-multistep form in Block-2nd Step in Full Size Page

Is their a way to begin Multi step Form in a Block and have the 2nd Step in a Full Size Page?

I am trying to have a form start in a block (Step 1) on my Home page and then in Step 2 I want to have some data displayed and ask for additional data in a full size page.

When I tried this using the Multipage forms I am sent back to the same block page rather than a larger page. Should I be trying to do this with Multipage Forms or another way?

Thanks

Wondering the same thing

Wondering the same thing right now... hmmm...

Thanks a lot

... for this great writeup... what I would actually love is have the whole self-updateing thing without a submit button, I mean AJAXed...

the moment the guy clicks the "gimme more" the form should populate itself with more stuff....
same for a field validation... the moment the user enters an url and leaves the field I want to perform the field validation
making sure it's really an url ....

that'd be great... any samples on how to do that ?

thanks,christoph

CCK Input?

I've seen some buzz about using this to make a wizard-style CCK input form. Being a drupal newbie, I have not figured out how this is done, as of yet. This is for a product node, to allow multi-page entry of product information for users.

Please help if you can, or point me in the right direction. I've looked at this and the "Theme a CCK input form" as well as monitored $form['#post'] but I still am not getting it.

Thanks.

form_values as array of arrays

First I must to say that I am new to drupal and PHP development, so excuse me if I say something stupid.

Whenn I post a form in multistep mode, I receive two $form_values arrays. One with the last posted data, and the second with the new new posted data. I don't understand the reason, since in your code, you always get the returned data with $form_values['x'].
In my case, if I print_r($form_values) I receive


Array\n(\n [x] => 4\n [op] => Select\n [form_build_id] => d6e62b57707a4f1b226b3360e28fce68\n [form_token] => 85aaec675c64a978e443d45a0b0ad62b\n [form_id] => _my_form\n)\n
Array\n(\n [x] => 3\n [op] => Select\n [form_build_id] => 6a946857fcf5462891e1d6df76375ef2\n [form_token] => 85aaec675c64a978e443d45a0b0ad62b\n [form_id] => _my_form\n)\n

the first with the old values (x=4) and the second with the new (x=3).

Thanks

Solutionated

Done. I have found that the form generation function is called twice.

Thanks

validation and hidden fields

I have difficulty accessing the value for a hidden field in the validation function.

I can access all the values for the form variables except the hidden 'step' value.

Must I use a different field type maybe?
Can I do validation in the form_sumbit function?

Any help would be appreciated.

do you do custom form development?

Hi there -
I run a site with two somewhat complicated forms - neither of which I wrote. I want to move to drupal, but porting those scripts is going to be a lot of trouble for me. Do you do paid custom coding work?

second step doen't get values passed

in Drupal 5.1 it seems that the form isn't getting the values passed the second time?

Multistep form with validation

I'm losing my mind. I've read tons of posts on multistep forms and on altering data in the validate function.

The design:

* multistep form
* node content type created in CCK
* using form_alter to hide/add/alter fields
* use custom validate and submit functions

The problem:

* if there are no errors, the concept is fine (step from one step to next)
* if there are validation errors, I'm trying to figure out a way to *communicate* that fact to the form_alter function so that the *step* stays the same instead of incrementing
* I think I'm going to have trouble having a "back" button too (since I want to save first) - I will need to know I'm going backwards instead of forwards

I have tried:

1) in validate, set $_SESSION variable... this isn't available when needed (though if you submit again) it does show up

2) in form_alter, add a placeholder form field; and in the validate function, pass in the $form as 3rd param and use form_set_value for the placeholder... the placeholder is empty when I get back to the form_alter function after submitting

3) in form_alter, check the drupal_get_messages to see if anything is there (says it's empty even though there are messages)

Any ideas?????????? I'm about to give up on the whole thing and just build my form from scratch instead of doing form_alter but maybe I will have the same issue there.

Thanks,
Kristen

form api

hi,
iam new to drupal.i created a form using form API.in this form i generated some fields with javascript.when iam submitting the form javascript generated fields are not visible in the $form_values array.
sorry for my poor english.please help me.
thanking you

When form['rebuild'] with other submits

On Drupal 6 (and perhaps not only), the method above works only when there aren't other submits present that rebuild the form with some changes. (like an 'add another item')
If they do, in D6 the solution is to declare a callback for a "next step" button. The step increment portion changes to:


if (!isset($form_state['values']['next_step'])) {
$step = 1;
}
else {
$step = $form_state['values']['step'] + 1;
}

$form['step'] = array(
'#type' => 'value',
'#value' => $step,
);

Then the "next step" button:

$form['next'] = array(
'#type' => 'submit',
'#submit' => array('form_next_step'),
'#value' => t('Next step'));

And the callback is:

function form_next_step($form, &$form_state) {
$form_state['values']['next_step'] = 1;
$form_state['rebuild'] = TRUE;
}

This seems like a very flexible approach.

contribute

This is the most helpful explanation of Form API I have read. Could this be contributed to the drupal.org and made available next to the form api reference and quickstart guides?
http://api.drupal.org/api/group/form_api/6
Also I would separate out the section which talks about FAPI 1 because new users don't need to know that.

Thanks for this helpful

Thanks for this helpful article.

How to change form on user's changes a input field.

Try to figure out how to do following:
1. a "select" filed with options of "today | yesterday | select your date" on form.
2. When use click on "select your date", a date input filed becomes visible for user enter a date.
Note: these all happen before user click on submit button.
Thanks ahead.

Client-side script

The easiest way to do that is client-side Javascript, not complex FormAPI processing. The SimpleViews module provides code that does just that -- when a user chooses to allow filtering, the 'type of filter' dropdown becomes visible. Check out the simpleviews edit form and the accompanying .js file for details...

ha, that's really quick

ha, that's really quick response. Thanks for your information. I will take a look.

Don't you need "&" in

Don't you need "&" in $form_values?

function my_form_submit($form_id, $form_values) {
$final_step = 10;

if ($form_values['step'] == $final_step) {
// Process the form here!
}

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd> <blockquote> <img> <i> <b> <strike> <h3> <h4>
  • Lines and paragraphs break automatically.
  • You may use [inline:xx] tags to display uploaded files or images inline.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Twitter-style @usersnames are linked to their Twitter account pages.
  • Twitter-style #hashtags are linked to search.twitter.com.

More information about formatting options

Miniblog

  • Totally got the third item in that list from @blakehall btw. He's the clever one! 44 min ago
  • There are two hard problems in CompSci: optimal cache invalidation, naming things, and off-by-one errors. 1 hour ago
  • OH: "Well, the Title title can just be the title, but reign_title can't be the reign title, or the title title." 4 hours ago
  • Know Drupal? Dig wrestling? Looks like the WWE is hiring... http://j.mp/bSu4pB 2 days ago
  • I want to be the Malcolm Gladwell of Drupal APIs. My breakout book will be named 'Clear Cache.' 4 days ago

SXSW Interactive 2011!