Sometimes, you have a special case where you want to have many different forms but only a single validation or
submit function. This is called code reuse, and it’s a good idea in that kind of a situation. The node module, for
example, runs all kinds of nodetypes through its validation and submission functions. So we need a way to map multipleform IDs to validation and submission functions. Enter hook_forms().
When Drupal is retrieving the form, it first looks for a function that defines the form based on the form ID (in our
code, we used the formexample_nameform() function for this purpose). If it doesn’t find that function, it invokes hook_forms(), which queries all modules for a mapping of form IDs to callbacks. For example, node.module uses the
following code to map all different kinds of node form IDs to one handler:
/**
* Implements hook_forms(). All node forms share the same form handler.
*/
function node_forms() {
$forms = array();
if ($types = node_get_types()) {
foreach (array_keys($types) as $type) {
$forms[$type .'_node_form']['callback'] = 'node_form';
}
}
return $forms;}
In our form example, we could implement hook_forms() to map another form ID to our existing code.
/**
* Implements hook_forms().
*/
function formexample_forms($form_id, $args) {
$forms['formexample_special'] = array( 'callback' => 'formexample_nameform');
return $forms;}
Now, if we call drupal_get_form('formexample_special'), Drupal will first check for a function named formexample_special() that defines the form. If it cannot find this function, hook_forms() will be called, and Drupal will see that we have mappedthe form ID formexample_special to formexample_nameform. Drupal will call formexample_
nameform() to get the form definition, and then attempt to call formexample_special_validate() and formexample_special_submit() for validation and submission, respectively.
Call Order of Theme, Validation, and Submission Functions
As you’ve seen, there are several places to give Drupal information about where your theme, validation, and submission
functions are. Having so many options can be confusing, so here’s a summary of where Drupal looks, in order, for a
theme function, assumingyou are using a theme named mytheme, and you’re calling drupal_get_form('formexample_
nameform'). This is, however, dependent upon your hook_theme() implementation.
First, if $form['#theme'] has been set to “foo” in the form definition then the order of checks that Drupal performs is as follows:
1. themes/mytheme/foo.tpl.php // Template file provided by theme.
2. formexample/foo.tpl.php // Template file provided by module.
3. mytheme_foo() // Function provided theme.
4. phptemplate_foo() // Theme function provided by theme engine.
5. theme_foo() // 'theme_' plus the value of $form['#theme'].
However, if $form['#theme'] has not been set in the form definition then the order is:
1. themes/mytheme/formexample-nameform.tpl.php // Template provided by theme.
2. formexample/formexample-nameform.tpl.php // Template file provided by module.
3. mytheme_formexample_nameform() // Theme function provided by theme.
4. phptemplate_formexample_nameform() // Theme function provided by theme engine.
5. theme_formexample_nameform() // 'theme_' plus the form ID.
During form validation, a validator for the form is set in this order:
1. A function defined by $form['#validate']
2. formexample_nameform_validate // Form ID plus 'validate'.
And when it’s time to look for a function to handle form submittal, Drupal looks for the following:
1. A function defined by $form['#submit']
2. formexample_nameform_submit // Form ID plus 'submit'.
Remember that forms can have multiple validation and submission functions.
Writing a Validation Function
Drupal has a built in mechanism for highlighting form elements that fail validation and displaying an error message to the
user. Examine the validation function in our example to see it at work:
* Validate the form.
*/
function formexample_nameform_validate($form, &$form_state) { if ($form_state['values'
['user_name'] == 'King Kong') {
// We notify the form API that this field has failed validation.
form_set_error('user_name',t('King Kong is not allowed to use this form.'));}}
Note the use of form_set_error(). When King Kong visits our form and types in his name on his giant gorilla keyboard, he
sees an error message at the top of the page, and the field that contains the error has its contents highlighted in red, as shown in Figure 11-6.
Figure 11-6. Validation failures are indicated to the user.
Perhaps he should have used his given name, Kong, instead. Anyway, the point is that form_set_error() files an error against our
form and will cause validation to fail. Validation functions should do just that—validate. They should not, as a general rule,
change data. However, they may add information to the $form_state array, as shown in the next section. If your validation
function does a lot of processing and you want to store the result to be used in your submit function, you have
two different options. You could use form_set_value() or $form_state.
Using form_set_value() to Pass Data
The most formal option is to create a form element to stash the data when you create your form in your form definition function,
and then use form_set_value() to store the data. First, you create a placeholder form element:
$form['my_placeholder'] = array( '#type' => 'value','#value' => array());
Then, during your validation routine, you store the data:
// Lots of work here to generate $my_data as part of validation.
...
// Now save our work.
form_set_value($form['my_placeholder'], $my_data, &$form_state);
And you can then access the data in your submit function:
// Instead of repeating the work we did in the validation function,
// we can just use the data that we stored.
$my_data = $form_state['values']['my_placeholder'];
Or suppose you need to transform data to a standard representation. For example, you have a list of country codes in the database
that you will validate against, but your unreasonable boss insists that users be able to type their country names in text fields.You
would need to create a placeholder in your form and validate the user’s input using a variety of trickery so you can recognize both
“The Netherlands” and “Nederland” as mapping to the ISO 3166 country code “NL.”
$form['country'] = array( '#title' => t('Country'), '#type' => 'textfield','#description' => t('Enter your country.'));
// Create a placeholder. Will be filled in during validation.
$form['country_code'] = array( '#type' => 'value','#value' => '');
Inside the validation function, you’d save the country code inside the placeholder.
// Find out if we have a match.
$country_code = formexample_find_country_code($form_state['values']['country']); if ($country_code) {
// Found one. Save it so that the submit handler can see it.
form_set_value($form['country_code'], $country_code, &$form_state);
}else {
form_set_error('country', t('Your country was not recognized. Please use a standard name or country code.'));}
Now, the submit handler can access the country code in $form_values['country_code'].
Using $form_state to Pass Data
A simpler approach is to use $form_state to store the value. Since $form_state is passed to both validation and submission
functions by reference, validation functions can store data there for submission functions to see. It is a good idea to use your
module’s namespace within $form_state instead of just making up a key.
// Lots of work here to generate $weather_data from slow web service
// as part of validation.
...
// Now save our work in $form_state.
$form_state['mymodulename']['weather'] = $weather_data
And you can then access the data in your submit function:
// Instead of repeating the work we did in the validation function,
// we can just use the data that we stored.
$weather_data = $form_state['mymodulename']['weather'];
You may be asking, “Why not store the value in $form_state['values'] along with the rest of the form field values?” That will
work too, but keep in mind that $form_state['values'] is the place for form field values, not random data stored by modules.
Remember that because Drupal allows any module to attach validation and submission functions to any form, you cannot make the
as sumption that your module will be the only one working with the form state, and thus data should be stored in a consistent and
predictable way.
Element-Specific Validation
Typically, one validation function is used for a form. But it is possible to set validators for individual form elements as well as for
the entire form. To do that, set the #element_validate property for the element to an array containing the names of thevalidation
functions. A full copy of the element’s branch of the form data structure will be sent as the first parameter. Here’s a contrived
example where we force the user to enter spicy or sweet into a text field:
// Store the allowed choices in the form definition.
$allowed_flavors = array(t('spicy'), t('sweet'));
$form['flavor'] = array( '#type' => 'textfield', '#title' => 'flavor','#allowed_flavors' => $allowed_flavors,
'#element_validate' => array('formexample_flavor_validate'));
Then your element validation function would look like this:
function formexample_flavor_validate($element, $form_state) {
if (!in_array($form_state['values']['flavor'], $element['#allowed_flavors'])) { form_error($element, t('You must enter spicy or sweet.'));}}
The validation function for the form will still be called after all element validation functions have been called.
Form Rebuilding
During validation, you may decide that you do not have enough information from the user. For example, you might run the form values through a textual analysis engine and determine that there is a high probability that this content is spam. As a result, you want todisplay the form again (complete with the values the user entered) but add a CAPTCHA to disprove your suspicion that this user is a robot. You can signal to Drupal that a rebuild is needed by setting $form_state['rebuild'] inside your validation function,like so:
$spam_score = spamservice($form_state['values']['my_textarea']; if ($spam_score > 70) {
$form_state['rebuild'] = TRUE;
$form_state['formexample']['spam_score'] = $spam_score;}
In your form definition function, you would have something like this:
function formexample_nameform($form_state) {
// Normal form definition happens.
...
if (isset($form_state['formexample']['spam_score']) {
// If this is set, we are rebuilding the form;
// add the captcha form element to the form.
...
}
...
}
Writing a Submit Function
The submit function is the function that takes care of actual form processing after the form has been validated. It executes only if
form validation passed completely and the form has not been flagged for rebuilding. The submit function is expected to modify
$form_state['redirect'].
function formexample_form_submit($form, &$form_state) {
// Do some stuff.
...
// Now send user to node number 3.
$form_state['redirect'] = 'node/3';}
If you have multiple functions handling form submittal (see the “Submitting the Form” section earlier in this chapter), the last
function to set $form_state['redirect'] will have the last word.
Changing Forms with hook_form_alter()
Using hook_form_alter(), you can change any form. All you need to know is the form’s ID. There are two approaches to alteringforms.
Altering Any Form
Let’s change the login form that is shown on the user login block and the user login page.
function formexample_form_alter(&$form, &$form_state, $form_id) {
// This code gets called for every form Drupal builds; use an if statement
// to respond only to the user login block and user login forms.
if ($form_id == 'user_login_block' || $form_id == 'user_login') {
// Add a dire warning to the top of the login form.
$form['warning'] = array('#markup' => t('We log all login attempts!'), '#weight' => -5);
// Change 'Log in' to 'Sign in'.
$form['submit']['#value'] = t('Sign in');}}
Since $form is passed by reference, we have complete access to the form definition here and can make any changes we want. In
the example, we added some text using the default form element (see “Markup” later in this chapter) and then reached in and
changed the value of the Submit button.
Altering a Specific Form
The previous approach works, but if lots of modules are altering forms and every form is passed to every hook_form_alter()
implementation, alarm bells may be going off in your head. “This is wasteful,” you’re probably thinking. “Why not just construct
afunction from the form ID and call that?” You are on the right track. Drupal does exactly that. So the following function will
change the user login form too:
function formexample_form_user_login_alter(&$form, &$form_state) {
$form['warning'] = array('#value' => t('We log all login attempts!'), '#weight' => -5);
// Change 'Log in' to 'Sign in'.
$form['submit']['#value'] = t('Sign in');}
The function name is constructed from this:
modulename + 'form' + form ID + 'alter'
For example,
'formexample' + 'form' + 'user_login' + 'alter' results in the following: formexample_form_user_login_alter
In this particular case, the first form of hook_form_alter() is preferred, because two form IDs are involved (user_login for the
form at http://example.com/?q=user and user_login_block for the form that appears in the user block).
Submitting Forms Programmatically with drupal_form_submit()
Any form that is displayed in a web browser can also be filled out programmatically. Let’s fill out our name and favorite color
program matically:
$form_id = 'formexample_nameform';
$form_state['values'] = array( 'user_name' => t('Marvin'), 'favorite_color' => t('green'));
// Submit the form using these values.
drupal_form_submit($form_id, $form_state);
That’s all there is to it! Simply supply the form ID and the values for the form, and call
drupal_form_submit().
Dynamic Forms
We’ve been looking at simple one-page forms. But you may need to have users fill out a form that dynamically displays elements
on the form based on selections the user made as he or she filled out the form. The following example demonstrates how to
display form elements dynamically as the user picks various options while filling out the form.
Start by creating a directory in your site/all/modules/custom folder named form_example_dynamic. In that directory, create a
form_example_dynamic.info file with the following information.
name = Form Example –Creating a Dynamic Form description = An example of a dynamic form. package = Pro Drupal Development
core = 7.x files[]=form_example_dynamic.module
Next create the form_example_dynamic.module file, and begin by placing the following header information in the file.
<?php
/**
* @file
* An example of how to use the new #states Form API element, allowing
* dynamic form behavior with very simple setup.
*/
With the header information in place, the next step is to create a menu item that a visitor can use to access the new form. The module provides a single menu entry that can be accessed via www.example.com/form_example_dynamic.
/**
* Implements hook_menu().
*/
function form_example_dynamic_menu() {
$items['form_example_dynamic'] = array( 'title' => t('Form Example Dynamic Form'), 'page callback' =>
'drupal_get_form','page arguments' => array('form_example_dynamic_form'), 'access callback' => TRUE,
'type' => MENU_NORMAL_ITEM);
return $items;}
With the menu complete, I’m now ready to create the form. The first item displayed on the form is a series of three radio buttons that allow a site visitor to select a room type to reserve.
function form_example_dynamic_form($form, &$form_state) {
$form['room_type'] = array( '#type' => 'radios',
'#options' => drupal_map_assoc(array(t('Study Room'), t('Small Conference Room'), t('Board Room'))),
'#title' => t('What type of room do you require?'));
The next form item is a fieldset that contains details about the study room and uses the #states attribute to determine whether this item should be displayed on the page. The #states attribute sets whether the field set will be visible by examining the room_type
radio buttons to see whether the Study Room option was selected. If the Study Room option was selected, then the value is set to true and the form will render the fieldset using jQuery. The syntax of the visibility test follows the syntax of using selectors in
jQuery. In this case, we’re looking at an input element (the radio buttons) named room_type. We’re examining whether the value
of the input is Study Room.
$form['study_room'] = array( '#type' => 'fieldset','#title' => t('Study Room Details'), '#states' => array('visible' => array(
':input[name="room_type"]' => array('value' => t('Study Room')),),),);
The next item shown on the form is two check boxes that allow a visitor to provide details about the types of equipment to be setup in the study room. In the example, I’ve limited those choices to chairs and a PC. I use the same #states approach as the
preceding field set. I want the check boxes displayed only if the visitor has selected Study Room from the list of available rooms.
$form['study_room']['equipment'] = array( '#type' => 'checkboxes', '#options' => drupal_map_assoc
(array(t('Chairs'), t('PC'))), '#title' => t('What equipment do you need?'),'#states' => array('visible' => array( // action to take.':input[name="room_type"]' => array('value' => t('Study Room')),),),);
If the user checked the Chairs check box, I’ll display a text field that allows the visitor to enter the number of chairs to be set up
in the room prior to his or her arrival. I’m using #action to control visibility of this text field, displaying the field only if the user
checked the Chairs check box.
$form['study_room']['chairs'] = array( '#type' => 'textfield','#title' => t('How Many Chairs Do You Need?:'), '#size' => 4,'#states' => array(
'visible' => array( // action to take.':input[name="equipment[Chairs]"]' => array('checked' => TRUE),),),);
The next element on the form is another text box that allows a visitor to enter details about the type of PC to be set up in the
study room. Like the foregoing chairs item, I’m using #action to control visibility by checking to see whether the visitorchecked the PC check box.
$form['study_room']['pc'] = array( '#type' => 'textfield','#title' => t('What Type of PC do you need?:'),'#size' => 15,
'#states' => array('visible' => array( // action to take.':input[name="equipment[PC]"]' => array('checked' => TRUE),),
),);
The next set of form elements is displayed only if the visitor clicked the “Small Conference Room” radio button. It follows the
same pattern of using the #actions attribute to determine whether form items should be visible based on a condition or action taken
by the visitor.
$form['small_conference_room'] = array( '#type' => 'fieldset', '#title' => t('small_conference_room Information'), '#states' => array('visible' => array(':input[name="room_type"]' => array('value' => t('Small Conference Room')),),),);
$form['small_conference_room']['how_many_pcs'] = array( '#type' => 'select',
'#title' => t('How many PCs do you need set up in the small conference room?'), '#options' => array(1 => t('One'),
2 => t('Two'),3 => t('Three'),4 => t('Four'),5 => t('Lots'),),);
$form['small_conference_room']['comment'] = array( '#type' => 'item', '#description' => t("Wow, that's a long time."),
'#states' => array( 'visible' => array(':input[name="how_many_pcs"]' => array('value' => '5'),),),);
$form['small_conference_room']['room_name'] = array( '#type' => 'textfield',
'#title' => t('Which room do you want to use?:'),);
$form['small_conference_room']['hours'] = array( '#type' => 'select',
'#options' => drupal_map_assoc(array(t('Free'), t('Paid'))),
'#title' => t('Do you want to reserve the room when it is free (no fees) or paid (prime time)?'),);
The following form element utilizes two conditional checks to determine whether the text field should be displayed. With #action youcan simply list out any number of conditions that must be met before the form item will be displayed. In this case, Icheck to see whether the visitor selected either Free or Paid from the preceding hours field.
$form['small_conference_room']['hours_writein'] = array( '#type' => 'textfield','#size' =>50,
'#title' => t('Please enter the date and time you would like to reserve the room and the duration.'),
'#states' => array('visible' => array( // Action to take: Make visible. ':input[name="hours"]' => array('value' =>
t('Free')), ':input[name="hours"]' => array('value' => t('Paid')),),),);
The reminder form item here introduces a new visibility check by verifying that the visitor seleted either Free or Paid and that heor she entered something in the hours_writein field.
$form['small_conference_room']['reminder'] = array( '#type' => 'item',
'#description' => t('Remember to enter the date, start time, and end time.'), '#states' => array(
'visible' => array('input[name="hours"]' => array('value' => t('Free')), 'input[name="hours"]' => array('value' =>
t('Paid')), 'input[name="hours_writein"]' => array('filled' => TRUE),),),);
$form['board_room'] = array( '#type' => 'fieldset','#title' => t('Board Room Information'), '#states' => array(
'visible' => array(':input[name="room_type"]' => array('value' => t('Board Room')),),),);
$form['board_room']['more_info'] = array( '#type' => 'textarea',
'#title' => t('Please enter the date and time of when you would like to reserve the board room'),);
$form['board_room']['info_provide'] = array( '#type' => 'checkbox',
'#title' => t('Check here if you have provided information above'), '#disabled' => TRUE,
'#states' => array('checked' => array(// Action to take: check the checkbox.':input[name="more_info"]' => array('filled' => TRUE),),),);
$form['expand_more_info'] = array( '#type' => 'checkbox','#title' => t('Check here if you want to add special instructions.'),);
$form['more_info'] = array( '#type' => 'fieldset','#title' => t('Special Instructions'), '#collapsible' => TRUE,
'#collapsed' => TRUE,'#states' => array( 'expanded' => array(':input[name="expand_more_info"]' => array('checked' => TRUE),),),);
$form['more_info']['feedback'] = array( '#type' => 'textarea',
'#title' => t('Please provide any additional details that will help us better serve you.'),);
$form['submit'] = array( '#type' => 'submit','#value' => t('Submit your information'),);\
return $form;
}
function form_example_dynamic_form_submit($form, &$form_state) { drupal_set_message(t('Submitting values: @values', array('@values' =>var_export($form_state['values'], TRUE))));}
With the module complete, I’ll enable the module and visit the form at www.example.com/form_example_dynamic. The first
page of the form should look like Figure 11-7.
Figure 11-7. The initial state of the form
Selecting Study Room from the list of options reveals the next part of the form (see figure 11-8), which asks
the visitor about the type of equipment to be set up in the room before he or she arrives.
Figure 11-8. Study Room Details fieldset is displayed based on the previous option selected.
Selecting the Small Conference Room option instead of Study Room displays the form elements related to the Small
Conference Room (see figure 11-9).
Figure 11-9. The Small Conference room form elements are displayed after
selecting Small Conference room from the room types.
If the visitor selects the Board Room from the list of room types, the details shown in figure 11-10 are displayed.
Figure 11-10. The Board Room elements are displayed after selecting Board Room from the list of room types.
Không có nhận xét nào:
Đăng nhận xét