Drupal 5: How to process multiple instances of the same form on the same page
This problem recently presented itself to me while working on a Drupal site which made heavy use of a non-ajaxed tabbed interface. With Drupal 5’s form API, form handling got a lot more agile, but the solution to this particular problem was not easy to find, though the API was able to solve my problem with a little digging.
The problem ?
Given a page with multiple instances of the same form (which use the same form id), your form submit() function may not receive the anticipated set of form values for the form the user has submitted. The values you get may not be from the form attached to the submit button the user clicked, but values from a different form with different values which is using the same form id.
Example: Below is the situation i found myself in. Given three functions, the form() function to create the form data, the submit() function to process the form values from the user when they click submit in their browser, and a view() function which renders two instances of the same form on one page.
/**
* The form creation function to create the form data needed
* by drupal_get_form() to render a form. The name of our
* function is normally what we will use as our form id to
* reference the form.
*/
function mymodule_thing_form($thing) {
# create form and add widgets to
# edit our thing
$form = array();
$form['thing']['name'] = ...;
$form['thing']['title'] = ...;
...
return $form;
}
/**
* Form submission function which Drupal looks for by
* using the form id and adding _submit() to the end.
* This is the function where our values will go when
* the user clicks the form submit button in their
* browser.
*/
function mymodule_thing_form_submit($form_id, $form_values) {
# save/update values in $form_values
...
}
/**
* For whatever reason, we want to show two thing edit
* forms on the same page, a separate form instance
* for each to edit $thing1 and $thing2.
*
* Drupal's drupal_get_form() will call our mymodule_thing_form()
* function above to get the form data to render the form.
*/
function mymodule_view($thing1, $thing2) {
$output = drupal_get_form("mymodule_thing_form", $thing1);
$output .= drupal_get_form("mymodule_thing_form", $thing2);
return $output;
}
The problem is that when the user clicks for example, on the submit button representing values for thing2 because that’s what they want to update, the submit() function may actually receive values for thing1 instead ! I’m not sure if this is a browser issue, or some value caching issue in the Drupal form API, but the result is not what you would expect initially. The HTML that Drupal renders will contain two
<form>HTML elements for thing1 and thing2 using
<form id="mymodule_thing_form">as the id. The only way around this is to use different form ids.
But how do we do this ? It’s the drupal_get_form() function handles all this for us and creates the form ids from the function name we gave it. We would have to create two separate functions with different names returning the same form data structure just to get this to work. We would also have to duplicate our submit() functions too because they would now be called
mymodule_thing_form1_submit()and
mymodule_thing_form2_submit()in order for Drupal to get the form values to us. Only two duplicate functions… not a big deal ! But what if you wanted to display 10 of the same form, or 20 on the same page… or you just don’t know because the number fluctuates based on the content of the site. What do you do now ehhhh ? (yes i am Canadian). How do you go about using different form ids for the same form ?
The solution
You use Drupal’s form hook function hook_forms() that’s what you do ! hook_forms() is called upon by the forms API when rendering a form for display in an HTML page. Given a form id, it returns an array containing the name of a callback function used to create the form data structure. It will be called upon by the Drupal form API once for every call to drupal_get_form(). NOTE: do not confuse hook_forms() with the singluar version hook_form() which serves a completely different function. We can use hook_forms() in the following way:
/**
* First, we need to change our view() function which outputs
* the rendered forms. We pass unique form ids into drupal_get_form().
* E.g. "module_thing_form3".
*/
function mymodule_view($things = array()) {
# we change our example to use an array of things which may vary from
# at any time. We use our counter $i to prefix our form id, so that
# drupal_get_form() can create a new form with a unique id.
for ($i=0; $i < count($things); $i++) {
$output = drupal_get_form("mymodule_thing_form" . $i, $things[$i]);
}
return $output;
}
/**
* How does drupal_get_form() know where to get the form data from ? For example,
* our view() function may call drupal_get_form() with a form id of
* mymodule_thing_form27. We don't have a function called mymodule_thing_form27(),
* so the forms API will call this form hook to get the name of a function that
* can provide form data. This is what prevents us from duplicating our _form()
* and _submit() functions for each unique form instance.
*
function mymodule_forms() {
# the form id Drupal plans on using will be the first
# element of the first array.
$args = func_get_args();
$form_id = $args[0][0];
# Given a form_id like mymodule_thing_form27 or mymodule_thing_form13,
# we look for familiar text to tell we are dealing with the correct
# form. Other functions will call this function as part of their form
# rendering (e.g. search form rendering) so we need to tell them apart.
$forms = array();
if (strpos($form_id, "mymodule_thing_form") === 0) {
# Here we are telling the forms API that for any form ids starting
# with mymodule_thing_formX (X could be any unique id), callback to the
# function mymodule_thing_form to get the form data.
$forms[$form_id] = array('callback' => 'mymodule_thing_form');
}
return $forms;
}
These two changes will now produce the same form in an HTML page, but the forms API will use different form ids which allows their submission data to be distinguished when the user submits data for a particular form. That’s it ! EXCEPT, one more thing…. we forgot about the submit() function. The forms API will look for the name of a submit() function which is prefixed by the form id. Example, on form submission of form with id
mymodule_thing_form56drupal will look for the submit function
mymodule_thing_form56_submit()to pass the value to. But we don’t have this function… and can’t create 56 of them just to ensure we cover our bases !
How do we let the forms API know that we want to submit our data to our one and only
mymodule_thing_form_submit()function ? (remember… we want to have only one if possible). We can use the super duper undocumented $form[’#submit’] directive to let the forms API know the function name to direct the values to on submission. We make the following small change to our form() function.
/**
* Slightly modified form function which sets the name for a
* submit() function callback when the form is submitted.
*/
function mymodule_thing_form($thing) {
# same as above
$form = array();
$form['thing']['name'] = ...;
$form['thing']['title'] = ...;
...
# important bit (note this is different from $form['submit']).
# this value is passed back to the forms API when the user
# submits and the API fill use this function to submit the
# user's values to.
$form['#submit'] = array("mymodule_thing_form_submit" => 1);
return $form;
}
The Drupal 5.0 form reference chart lists
#submitas a “Special Element” which is only to be used with button and submit contexts form elements, but i found this value was also looked for by the forms API when submitting data. Will this always be the case ? Who knows… but the term “Special” is an understatement in our case. It should really be called “Super Special” would be more appropriate.
So, that’s it ! Our complete set of changes look something like this (don’t try to run this at home without a vigorous syntax check).
/**
* Create form data structure used to render a form.
*/
function mymodule_thing_form($thing) {
# same as above
$form = array();
$form['thing']['name'] = ...;
...
# important bit (note this is different from $form['submit']).
$form['#submit'] = array("mymodule_thing_form_submit" => 1);
return $form;
}
/**
* hook_form() formid->callback mapping magic !
*/
function mymodule_forms() {
# get the form id
$args = func_get_args();
$form_id = $args[0][0];
# Ensure we map a callback for our form and not something else
$forms = array();
if (strpos($form_id, "mymodule_thing_form") === 0) {
# Let the forms API know where to get the form data corresponding
# to this form id.
$forms[$form_id] = array('callback' => 'mymodule_thing_form');
}
return $forms;
}
/**
* Our view function which creates multiple instances of
* the same form using different form ids.
*/
function mymodule_view($things = array()) {
# for each thing, create a new form with a unique id.
for ($i=0; $i < count($things); $i++) {
$output = drupal_get_form("mymodule_thing_form" . $i, $things[$i]);
}
return $output;
}
/**
* Standard form submit() function.
*/
function mymodule_thing_form_submit($form_id, $form_values) {
# save/update values in $form_values
...
}




