The attached patch accomplishes five things.

  1. The form redirection code works again. (same code as http://drupal.org/node/79614 -- it was just a pain not having it fixed. If that gets in quickly, I can re-roll the patch.)
  2. Values that were used to build a form (parameters passed to drupal_get_form(), for example) are stored in $form['#params']. This is very useful for http://drupal.org/node/79900, Adrian's patch to allow recording of form input. It can store build params rather than the entire form.
  3. There's now a drupal_execute($form_id, $form_values) function that easily wraps programmatic form submission. drupal_get_form() for stuff that will be displayed to the user, drupal_execute() for stuff done programmatically.
  4. When submitting a form programmatically, default values (like 'Published' and 'Promoted to front page' for nodes) are properly set and processed.
  5. Last but not least, drupal_get_form() is now capable of handling multi-step forms that loop repeatedly, presenting a different set of form fields each time (perhaps depending on the results of the previous step). Normally, the problem with this is that incoming form values DIFFER from the form that is currently being built (step 1's values get validated against step 2's fields, and hijinks ensue). With this patch, the build-params of the previous step are stored in the session, and 'step 1' is reconstituted for validation and submission before 'step 2' is rendered and displayed.

    All that's necessary to take advantage of this process is setting the #multistep flag on your form array to TRUE when it's built. From there on, you can do whatever strange and diabolical dynamic form things you desire, secure in the knowledge that drupal will match up submitted values with the form that created them. It's simple and straightforward, and there are copious comments explaining the whys and hows. Thanks to chx for hammering out the meat of this code.

  6. Reviews appreciated. This set of changes will help tie up the loose ends in FormAPI and leave us with a MUCH MUCH improved set of functionality for 4.8/5.0.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

webchick’s picture

!!!

adding to my test queue

eaton’s picture

FileSize
8.22 KB

Here's an updated version. No code changes, just corrected the documentation in the header of form.inc to use drupal_execute rather than the outdated retrieve, populate, process trio.

eaton’s picture

FileSize
8.09 KB

One more version. two unecessary lines removed from drupal_get_form() -- they were vestigal remains of a previously used technique, no longer needed.

I haven't had a chance to whip up a sample module yet, but this function would allow a single form_id to generate a form that keeps submitting to itself, iteratively changing the form definition, submitting and validating differently each time, until it's finished. One example:

function my_form($form_values = array()) {
  $form['#multistep'] = TRUE;

  $current_step = $form_values['step']++;
  $form['step'] = array(
    '#type' => 'hidden',
    '#value' => $current_step;
  );
  
  switch ($current_step) {
    case 1:
      $form['title'] = array(
        '#type' => 'textfield',
        '#title' => t('Title'),
      );
      break;

    case 2:
      $form['title'] = array(
        '#type' => 'hidden',
        '#value' => $form_values['title'],
      );
      $form['body'] = array(
        '#type' => 'textarea',
        '#title' => t('Body'),
      );
      break;

    case 3:
      $form['title'] = array(
        '#type' => 'hidden',
        '#value' => $form_values['title'],
      );
      $form['body'] = array(
        '#type' => 'hidden',
        '#value' => $form_values['body'],
      );
      $form['notes'] = array(
        '#type' => 'textarea',
        '#title' => t('Other notes'),
        '#default_value' => 'Pre-populated with Hello Kitty info.',
      );
      if ($form_values['title'] == 'Hello Kitty') {
        $form['notes']['#default_value'] = 'Pre-populated with Hello Kitty info.';
      }
      break;
    default:
      $form['huh'] = array(
        '#type' => 'markup',
        '#value' => t('Something goofy happened.'),
      )
  }
  
  return $form;
}

The above function builds a form. It sets the #multistep behaviour to true, activating the special handling in drupal_get_form. It also keeps a hidden field that stores the current step that it's on. Depending on which one it's on, it builds one of three fields, and stores the values from the previous step in hidden fields. In the third step, it pre-populates a text field with different values depending on the values entered in the second step.

Similar switching logic would be necessary in the validate and submit functions, but judicious use of sub-functions could simplify that.

In some cases, authors might want to validate on each *step* but only submit on the final step, with the values all accumulated in hidden fields. In others, authors might want to validate and submit each step of the way. It's up to them -- this patch just makes sure that subch techniques don't stumble and fall when it comes time for processing.

Dries’s picture

Isn't 'wizard' a better name than 'mutistep' or 'multistep form'?

Please use 'parameters' rather than 'params'.

We'll want to explain high-level things that are obvious for you but not for others. For example, we'll want to document whether the form values are stored at the last step, or whether things are stored and validated partially/incrementally.

Then, I'd document the working in a bit more detail (also in PHPdoc). For example, how is information carried from one 'step' (page) to another? Is it stored in hidden variables, or stored in a session (and not send back to the client). This information is valuable has it helps people wrap their head around it.

Furthermore, it might also be important with regard to security. You might not want to save VISA-card information in a hidden form field (or maybe it doesn't really matter as you already sent the information over the wire).

gordon’s picture

Keeping an eye on this for E-Commerce.

1 Question.

Is it possible to leave the multi form process and go to a screen outside the current form, and then return the same possition.

An example is in the E-Commerce checkout process, the are stages like the address page in which the user will leave the checkout and return same possition to complete the checkout.

Also payment gateways like paypal pro expect you to leave the entire site, and return to the next step.

eaton’s picture

Dries:

Isn't 'wizard' a better name than 'mutistep' or 'multistep form'?

Wizards are one application of multi-step form code. Really large forms divided into bite-sized pieces are another. Single forms (like views.module) that keep submitting to themselves, building more fields depending on user input, until everything finally gets submitted, are another. I think naming it 'wizard' would imply that it offers more functionality in that particular application, and would mask its usefulness in others.

Please use 'parameters' rather than 'params'.

Good point. I'll change it.

We'll want to explain high-level things that are obvious for you but not for others. For example, we'll want to document whether the form values are stored at the last step, or whether things are stored and validated partially/incrementally...

Those things are all left to the developer. I'll step back for a moment to explain the underlying problem we currently gace with multipart forms:

  1. We can, today, make forms that submit to themselves and build different contents based on the incoming post values.
  2. Unfortunately, because form building happens before validation and submission, it means that we get out of sync. The incoming values correspond to the previous step's form. Other than re-implementing form workflow entirely in your custom module, there's no way around that.
  3. This change stores in the session only the data necessary to reconstitute the previous form. In a multistep form scenerio, it validates and submits using the previous form. If validation fails, it displays that form again. If validation passes, it displays the 'current' form.

Then, I'd document the working in a bit more detail (also in PHPdoc). For example, how is information carried from one 'step' (page) to another? Is it stored in hidden variables, or stored in a session (and not send back to the client). This information is valuable has it helps people wrap their head around it.

Only the build parameters are stored. If a module wants to validate and process each set of form_values as they come in, no problem. If it wants to validate them at each step, then queue them into hidden fields and submit them all at once, no problem. It would just have to do that in its own form_builder function.

I agree this needs to be explained, but it's also less 'magical' than it might appear. It's not about automating the creation of wizards so much as removing a roadblock that prevented them from working at all. I looked into a number of 'automatic' ways of handling that stuff, and all led to frightening code bloat AND imposed a lot of restrictions on module writers.

Gordon:

Is it possible to leave the multi form process and go to a screen outside the current form, and then return the same possition. An example is in the E-Commerce checkout process, the are stages like the address page in which the user will leave the checkout and return same possition to complete the checkout.

Using this code, it would not be automatically handled by core but it *would* be possible. How would you do it?

  1. Use multi-step mode to walk through the checkout, accumulating data in hidden forms.
  2. When the time comes to jump out to paypal, gather up your current set of form variables. Create a 'key' for them, perhaps the user's session ID plus some salt number, or perhaps a transaction ID, and store the current set of form variables in the session with that key.
  3. When redirecting FROM paypal back to drupal, append that key onto the URL.
  4. In your form building function, check for the presence of said key in arg(). If it exists, use it to pull up your saved set of form values and 'resume' the process where you left off.
chx’s picture

To make it very clear: this is just the minimal necessary code. Multipart forms are just not happenning often enough to mandate lots of complex coding. But these lines, as Eaton very throughly explains is absolutely necessary scaffolding.

See http://groups.drupal.org/node/100 for the discussion. And note that the form pull patch was directly sparkled by this one because when I coded this and discussed with Eaton he wanted to store some kind of 'seed' instead of the whole form in the session. I realized that his wish is exactly what Adrian described earlier as pull modell as something that's useful for a lot of things. So there.

eaton’s picture

FileSize
7.28 KB

Re-rolled for HEAD. Also changed #params to #parameters.

Regarding documentation, I'm now writing up a brief document that outlines different approaches to creating multistep and dynamic forms with these new capabilities. The subject is focused enough (and in-depth enough) that trying to put docs into form.inc would be a mistake, IMO. The code comments in there right now explain what's going on when session vars are being set, but don't attempt to provide tutorials on making multistep forms.

gordon’s picture

Eaton: Thanks for this, I think that it will work very well. Over the next month I am going to be rewritting the checkout to use the new functionality.

Dries’s picture

Ok, if we don't all do that, why do we use sessions and not a hidden form field? Using sessions has bigger performance cost as it will require two or more database queries. Can't we use form fields instead?

chx’s picture

putting a function name and its parameters into hidden fields? does not sound like secure here. session is safe... and I fail to see how we get more DB queries, forms are mostly used by logged in users who have their sessions saved anyways. this does not apply to all forms, only those that have #multistep set.

eaton’s picture

We don't use hidden form fields because (1) they're harder to persist in situations like the ones Gordon described, and (2) the parameter objects are potentially more complex than simple strings. In some cases, they may be node objects or other data that would require serialization.

Using hidden fields to control the build parameters also opens us up to odd and wacky exploits, where a user who mucks around with the hidden fields in a form before submitting can directly control what params we're passing around to our build functions. The chances of attack via that vector aren't huge, but It's a definite possiblity considering what data we would expose.

eaton’s picture

Also what chx said. :)

chx’s picture

FileSize
9.34 KB

Reroll for HEAD

It's a bit bigger because I needed to indent a big chunk of text.

We have agreed with Moshe that in programmed forms if you want to change something then you explicitly set it. That's an important convention because when you post from HTML, an unchecked checkbox does not appear in POST. However, when you program it, if you do not set the relevant value in #post, then you want to leave it alone. This was also in former versions of this patch, just wanted to explain.

moshe weitzman’s picture

FileSize
1.53 KB

code looks good, and is documented. i tried a bunch of forms with no ill effects. so all that is left IMO is to actually exercise the multistep handling. so i turned eaton's earlier code into a module. unfortunately the module has some unlrelated problem in it. i post it here anyway for others to frown upon.

moshe weitzman’s picture

I should add that this is essential for the admin/views form, which works around fapi right now.

adrian’s picture

Status: Needs review » Reviewed & tested by the community
drumm’s picture

Status: Reviewed & tested by the community » Needs review

This all looks good and well-documented (assuming that separate multistep thing is posted), but looks like at least 3 patches crammed into one to me:

1. skip a large chunk of form_builder() if we are programmed.
2. add drupal_execute().
3. various multistep changes.

drumm’s picture

Status: Needs review » Needs work
moshe weitzman’s picture

@drumm - are you requiring 3 separate patches in order to commit this? you just said that it all looks good. what is the point of this exercise? perhaps you could just inform the authors: "next time, please submit separate patches when there is no dependency between them".

eaton’s picture

The patch was originally conceived as one to 'fix the loose ends remaining after the recent round of formAPI refactorings.' It fixes the use of default values, allows multistep forms to work, and simplifies programmatic use of forms.

If absolutely necessary, I can split them up. It seems sad to add another round of post/review/discuss/debate/RTBC/debate/review/etc to three individual patches just because they were originally submitted as one. They seem (to me at least) all pretty closely related to the recent round of Form refactorings.

eaton’s picture

Title: FormAPI: Multipart forms and more » FormAPI: Multipart forms
Status: Needs work » Reviewed & tested by the community
FileSize
4.12 KB

Attached is *JUST* the multistep form handling portion of the patch. Setting back to RTBC, as no code changes have been made.

eaton’s picture

The other two portions have moved to:

http://drupal.org/node/80470
http://drupal.org/node/80471

drumm’s picture

Committed to HEAD.

drumm’s picture

Status: Reviewed & tested by the community » Fixed

Now document it.

eaton’s picture

http://jeff.viapositiva.net/drupal/dynamic-forms contains a first draft of the documentation. I'll be working on rolling that into something less 'chatty' and more like a reference guide for inclusion in the handbook.

Anonymous’s picture

Status: Fixed » Closed (fixed)