Creating multi-part forms
There's no question that the new forms API will make life easier for Drupal developers. However, not everything about them is intuitive. Creating multi-part forms is one of them. I spent many, many hours trying to figure out the best way to do this. Granted, I'm not a hard-core coder like some others Drupalers out there, but I think even advanced coders will also have their share of frustration. And so to help you avoid the pain I went through, I offer this tip. Note that what follows assumes you are familiar with forms API.
It wasn't until I studied the code in node.module that I was actually able to figure this out. Specifically, the three functions in that module that I learned from were node_admin_nodes(), node_multiple_delete_confirm(), and node_multiple_delete_confirm_submit(). Together, these functions allow users to select which nodes to delete (part 1 of the multi-part form) and then confirm the deletion (part 2 of the the form).
But rather than use that module as a basis for this discussion, I created a simple module that simulates those functions more compactly to avoid losing the forest through the tangles of code in node.module.
Anyway, here's the boiled-down, multi-part form simulation module:
<?php
function formtest_menu() {
if (!$may_cache) {
$items[] = array('path' => 'formtest', 'title' => t('initial form'),
'callback' => 'formtest_page',
'access' => TRUE,
);
}
return $items;
}
// Main function
function formtest_page() {
// If user has already performed an operation, handle it.
if ($_POST['op'] == 'Submit' || $_POST['edit']['confirm']) {
return formtest_confirm_form();
}
// Main form
// This is a parent element so #tree is set to TRUE
$form['checkboxes'] = array(
'#tree' => TRUE,
);
// Checkbox #1 child element
$form['checkboxes'][1]['checked'] = array(
'#type' => 'checkbox',
'#title' => 'Checkbox #1',
'#default_value' => 0,
);
$form['checkboxes'][1]['title'] = array(
'#type' => 'hidden',
'#value' => 'Checkbox #1',
);
// Checkbox #2 child element
$form['checkboxes'][2]['checked'] = array(
'#type' => 'checkbox',
'#title' => 'Checkbox #2',
'#default_value' => 0,
);
$form['checkboxes'][2]['title'] = array(
'#type' => 'hidden',
'#value' => 'Checkbox #2',
);
$form['submit'] = array(
'#type' => 'submit',
'#value' => 'Submit',
);
return drupal_get_form('simple_form', $form);
}
function formtest_confirm_form() {
$edit = $_POST['edit'];
// Here, we create a form element that will hold the $_POST data
// which contains an array representing the checkboxes the user checked.
// Notice we give the first dimension of this element the name "checkboxes".
// This is the same name as the parent form element in the formtest_page.
// Giving it the same name allows us to add children to the parent (with $_POST
// acting as our surrogate parent). We must do this so we can pass our data on
// to the formtest_confirm_submit().
$form['checkboxes'] = array('#tree' => TRUE);
// Now we populate the element with our data.
foreach ($edit['checkboxes'] as $checkbox_id => $data) {
if ($data['checked']) {
$form['checkboxes'][$checkbox_id] = array(
'#type' => 'hidden',
'#value' => $checkbox_id,
);
$checked[] = check_plain($data['title']);
}
}
$form['data'] = array(
'#type' => 'markup',
'#value' => theme('item_list', $checked),
);
$form['operation'] = array(
'#type' => 'hidden',
'#value' => 'delete',
);
$output = confirm_form('formtest_confirm', $form,
t('delete these boxes?'),
'formtest', t('This action cannot be undone.'),
t('Delete'), t('Cancel')
);
return $output;
}
function formtest_confirm_submit($form_id, $edit) {
//Simulate deletion of our data
_formtest_simulate_deletions($edit);
//Return user back to main page.
drupal_goto('formtest');
}
//This code is for simulation purposes only.
function _formtest_simulate_deletions($edit) {
drupal_set_message('If this code was real, the checkbox(es) you deleted would not appear.');
// Code to delete data here
}
?>You can actually install this chunk of code in your modules directory and see it work. At the time of this writing, you'll need cvs but by the time you read this, it will likely work with version 4.7+ of Drupal.
The code basically speaks for itself. There are a few tricky catches in there. Lengthy comments have been supplied where this is the case. The three functions to focus on are formtest_page(), formtest_confirm_form(), formtest_confirm_submit(). The other functions aren't important to creating multi-part forms.
This code is extremely simple. A more realistic example would be more complex and require to be very careful to run security checks on the $_POST data which ultimately ends up in our form. Under normal circumstances, this is a no-no.
If anyone can offer suggestions on how to improve this code or simplify it, please share!
This tip courtesy of: Steve Dondley, Dondley Communications with special thanks to Earl Miles (a.k.a. merlinofchaos) for guidance offered.
Or try this one - a bit of a variation, but fairly similar.
uses a function (_pagehandler) to work out which form to display depending on what had just been submitted.
Also records the submitted data into a sessionObject which is refered to in the final page (format_page_c)
semi-intelligent, works good for multi forms, split forms etc as it does the validation too!
<?php
function formtest_menu() {
if (!$may_cache) {
$items[] = array('path' => 'formtest', 'title' => t('initial form'),
'callback' => 'formtest_pagehandler',
'access' => TRUE,
);
}
return $items;
}
function _post_to_sessObj($POSTcontext,$sessObj) {
session_start();
foreach($POSTcontext as $key => $value) {
$_SESSION["edit"][$key]=$value;
}
return $sessObj;
}
function formtest_pagehandler() {
// define your sequence of forms here
$formSeq=array('formtest_page_a','formtest_page_b','formtest_page_c');
$edit=$_POST["edit"];
session_start();
if(isset($_POST["edit"]) ) {
$form=$formSeq[$_SESSION["submitFormNo"]]();
$old=drupal_get_form($formSeq[$_SESSION["submitFormNo"]],$form);
if(!isset($_SESSION["messages"]["error"])) {
_post_to_sessObj($edit); // save that form to sessObj
$_SESSION["formNumber"]++; // goto next form
}
}
if ($_SESSION["formNumber"] > sizeof($formSeq)-1 || $_SESSION["formNumber"] <0 || !isset($_SESSION["formNumber"]) ||!isset($edit)) { //safety catch
unset($_SESSION["edit"]); // clear the session object
$_SESSION["formNumber"]=0;
$_SESSION["submitFormNo"]=0;
}
$form=$formSeq[$_SESSION["formNumber"]]();
$_SESSION["submitFormNo"]=$_SESSION["formNumber"]; // record for validating
if(is_array($form)) { // if the function returned a proper formAPI
$content=drupal_get_form($formSeq[$_SESSION["formNumber"]],$form);
} else { // if the function returned something else (a HTML string?)
$content=$form;
}
return $content;
}
function formtest_page_a() {
$content="Continue to page B?<P>";
$form['pageID'] = array('#type' => 'hidden','#value' => '1' );
$form['name'] = array('#type' => 'textfield', '#value' => 'Your name' );
$form['submit'] = array('#type' => 'submit', '#value' => 'Submit' );
$content.=drupal_get_form('simple_form', $form);
return $content;
}
function formtest_page_b() {
$content="Continue to page C?<P>";
$form['pageID'] = array('#type' => 'hidden','#value' => '2' );
$form['address'] = array('#type' => 'textfield', '#value' => 'Your address' );
$form['submit'] = array('#type' => 'submit', '#value' => 'Submit' );
$content.=drupal_get_form('simple_form', $form);
return $content;
}
function formtest_page_c() {
$content="<h2>final page!</h2>";
foreach($_SESSION["edit"] as $key => $value) {
$content.="$key => $value<BR>";
}
unset($_SESSION["edit"]); // clear the session object
return $content;
}
?>See also: Multipage forms with the Form API (4.7)
