drupal lightning fapi jumpstart
DESCRIPTION
Session presented 10/24 at Pacific Northwest Drupal Summit discussing Drupal form APITRANSCRIPT
Lightning (It's gonna be fast paced)
FAPI (Expect lots of code)
Jumpstart (Feel the power!)
Doug Stumbergerdrupal.org = twitter = facebook = ‘dougstum’
DND Communicationswww.raceonedesign.com
dstumberger @ hotmail.com
It's a Drupal talk, so…
*
* No cats were harmed in the creation of these slides.
Don't recognize this? Uh-oh.
Something simple to begin withfunction newsletter_subscribe_form($form_state) { $form = array(); $form['email'] = array( '#type' => 'textfield', '#title' => t('E-mail address'), // use t() for localization '#size' => 64, '#maxlength' => 64, '#required' => TRUE, '#weight' => -10, ); $form['subscribe'] = array( '#type' => 'radios', '#title' => t('Subscribe'), '#default_value' => 0, '#options' => array(t('Yes'), t('No')), '#weight' => 5, ); $form['submit'] = array( '#type' => 'submit', '#value' => t('Save'), // D6; use “Save” instead of “Submit” (#287986) '#weight' => 20, ); return $form; }
Validate & submit Validate
Default handler: FORMID_validate ($form, &$form_state) Modify $form['#validate'] array in form definition or hook_form_alter Use form_set_error to signal validation errors associated with the form Also useful: form_get_error($element) and form_get_errors() Required fields are handled automatically, no need to check in validate handler
Submit Default: FORMID_submit ($form, &$form_state) Modify $form['#submit'] array in form definition or hook_form_alter Particularly useful for redirecting the user (default is to return to form) drupal_goto($path = '', $query = NULL, $fragment = NULL, $http_response_code = 302)
// Block redirection (returns to form)$form_state['redirect'] = FALSE;
// Redirect to site homepage$form_state['redirect'] = '<front>';
// Redirect to relative path from base_path$form_state['redirect'] = 'node/' . $node->nid;
// Specify arguments to drupal_goto()$form_state['redirect'] = array($node->path, drupal_get_destination());
Passing data to validate & submit Pass values from form definition to validate and submit handlers
$form['node'] = array( '#type' => 'value', '#value' => $node, );
Once form is submitted, retrieve data from $form_state['values']function subscribe_form_validate($form, &$form_state) { $node = $form_state['values']['node'];};
Pass values (or modify passed values) from validate to submit handlerfunction subscribe_form_validate($form, &$form_state) { $form_state['values']['node']->title = t('Hello World');};
-- OR –-function subscribe_form_validate($form, &$form_state) { $node = $form_state['values']['node']; $node->title = t('Hello World'); form_set_value($form['node'], $node, $form_state);};
Once form is submitted, retrieve data from $form_state['values'] $node = $form_state['values']['node'];
Fieldsets and trees (and access!) Collapsible groupings of form fields
$form['options'] = array( '#type' => 'fieldset', '#access' => user_access('administer nodes'), '#title' => t('Publishing options'), '#collapsible' => TRUE, // Default is False. '#collapsed' => TRUE, // Default is FALSE. '#tree' => FALSE,);
$form['options']['status'] = array( '#type' => 'checkbox', '#title' => t('Published'), '#default_value' => $node->status,);
Default: values are “collapsed” when #tree = FALSEfunction mymodule_form_submit($form, &$form_state) { $status = $form_state['values']['status'];};
But when '#tree' = TRUE, you must specify complete path to value $status = $form_state['values']['options']['status'];
Tabular forms Widely used for admin forms function request_form($form_state){
$form = array(); foreach ($requests as $request_id => $request) { $form['request'][$request_id]['#tree'] = TRUE; $form['request'][$request_id]['delete'] = array( '#type' => 'checkbox', '#default_value' => 0, ); $form['request'][$request_id]['user'] = array( '#value' => theme('username', user_load(array('uid' => $request->uid))), ); $form['request'][$request_id]['time'] = array( '#value' => date('j M H:I', $request->timestamp), ); $form['request'][$request_id]['operations'] = array( '#value' => l('Add article', 'newsletter/add_article', 'id=' . $request_id) ); } }
Tabular forms: theme Use form theming to convert into table markup
function theme_request_form($form) { $headers = array( '', t('User'), t('Time'), t('Operations') );
$rows = array();
foreach ($form['add'] as $request_id => $request) { $rows[] = array( drupal_render($form['request'][$request_id]['delete']), drupal_render($form['request'][$request_id]['user']), drupal_render($form['request'][$request_id]['time']), drupal_render($form['request'][$request_id]['operations']), ); } return '<h3>' . t('Requests') . '</h3>' . theme('table', $headers, $addrows)
. drupal_render($form); }
Remember, FAPI will automatically look for theme_FORMID
Custom elements Define a custom element with fields to be merged function fcklite_elements() { return array('fcklite_textarea' => array( '#input' => TRUE, '#prefix' => "<div class='fcklite_textarea'>", '#suffix' => '</div>', '#rows' => 20, )); } Theme your element (remember to add to hook_theme) function theme_fcklite_textarea($element) { return theme('form_element', array( '#title' => $element['#title'], '#description' => $element['#description'], '#id' => $element['#id'], ), FCKeditor_IsCompatibleBrowser() ? fcklite_open_textarea($element['#id'], $val, $element['#rows']) : t('Incompatible browser') ); } D7: hook_elements -> hook_element_info and
hook_element_info_alter (#572932)
A custom fckeditor element
Invocation: drupal_get_form Primary means of invoking Drupal forms programmatically
Manages building, theming, presentation, execution
Returns HTML (at least in Drupal 6!)$html = drupal_get_form('newsletter_subscribe_form');
drupal_set_content(‘column_1’, $html);
Frequently used as a page callback for hook_menu itemsfunction newsletter_menu() { $items['newsletter/subscribe'] = array( 'page callback' => 'drupal_get_form', 'page arguments' => array('newsletter_subscribe_form'), 'access arguments' => array('manage subscriptions'), ); return $items;}
drupal_get_form: default values Use your hook_menu wildcards and loaders!
function newsletter_menu() { // Note: %user wildcard loader = user_load $items['newsletter/subscribe/%user'] = array( 'page callback' => 'drupal_get_form', 'page arguments' => array('subscribe_form', 2), 'access arguments' => array('manage subscriptions'), ); return $items;}
Remember to use $form_state as first parameter (almost always!)
function newsletter_form($form_state, $objUser) { $form = array( 'email' => array( '#type' => 'textfield', '#title' => t('Email address'), '#default_value' => $objUser->mail, ));};
Add/edit forms for node types Use hooks to change default behavior for two URLs
Create URL: www.example.com/node/add/TYPE Edit URL: www.example.com/node/%node/edit
Replace standard node create/edit forms Define function TYPE_node_form($form_state, $node) Use node_form_validate & node_form_submit OR customize by specifying $form['#submit][] or $form['#validate'][] To theme, either use default: theme_node_form OR implement theme_TYPE_node_form (don't forget to add to hook_theme)
Or add custom fields to standard node create/edit forms Only works with node types defined by a module (not through admin) Implement hook_form ($node, $form_state) // NOTE REVERSED ARGUMENTS Uses node_form_validate & node_form_submit Themed using theme_TYPE_node_form or theme_node_form
Form factories using hook_forms Multiple modules can use the same form function Each invocation can use a different form_id Modules can alter submission, validation etc.
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;}
Calls to TYPE_node_form will result in calls to function node_form(...) with form_id = TYPE_form
Modifying forms: hook_form_alterfunction newsletter_form_alter(&$form, $form_state, $form_id) { // Switch statement is also commonly used. if ($form_id == 'newsletter_subscribe_form') {
// Add elements to the form. $form['name'] = array( '#type' => 'textfield', '#title' => t('Name'), );
// Remove elements from the form. unset($form['phone_number']);
// Add a new submit handler (or replace altogether). $form['#submit'][] = 'mymodule_submit_handler';
// Replace existing validation with your code (or add validation). $form['#validate'] = array('mymodule_validate_handler');
// Move form items around using weights. $form['email']['#weight'] = -3;
// Fix tabindex problems, add prefix/suffix, etc. $form['submit']['#attributes']['tabindex'] = '3';
}
}
See for example login_toboggan.module Also explore: #after_build, #pre_render, #post_render
Theming forms & elements Goal: match the look of a form to its design or layout context
Leave markup the same but modify the CSS (Use FIREBUG!!!)
Use #prefix and #suffix fields on a form or on an individual element. Especially useful when set using hook_form_alter.
$form['access'] = array( '#type' => 'textfield', '#title' => t('Log'), '#prefix' => '<div class="inline_title">', '#suffix' => '</div>',);
Override the default individual element theme functions
function MY_THEME_NAME_radio($element) { ... }
A default theme function exists for every visible element type
But, this overrides the theming for every element of that type on every form
Can also override theme_form and theme_form_element!
#theme…the hammer, er, scalpel Specify theme function on whole form or individual element
$form = array( '#theme' => 'table_based_form', );
Make sure the theme function is defined in hook_theme
Remember, you can add '#theme' during hook_form_alter
The usual algorithm for theming forms:
Loop through all elements in a form
Output the element's surrounding markup, description, etc.
Render the element with drupal_render($form[$element_id])
Render the remainder of the form with drupal_render($form)
You can pass parameters into the theming using custom form elements and custom '#' fields on individual elements
$form['fb_fields'] = array( '#type' => 'value', '#value' => array('title', 'body'), );
$form['sender_email'] = array( '#type' => 'item', '#fb_divider' => TRUE, );
Here, a standard imagefield form becomes both more functional and more consistent in presentation with the rest of the site.Combining
form_alter and theming gives you the power to dramatically modify contrib module forms, icluding CCK-based forms. (after_build helps!)
Multi-step forms Multi-page surveys, wizards, notifications, etc.
First, conditionalize UI elements, especially for form navigation
function newsletter_subscribe_form($form_state) { $form = array();
// Second time through the form builder.
if (isset($form_state['storage']['confirm'])) {
$form['confirm_message'] = array( '#type' => 'item', '#value' => t('Email address !email is subscribed.', array('!email' => $form_state['storage']['email'])), );
}
// First time through the form builder.
else {
$form['email'] = array( '#type' => 'textfield', '#title' => t('E-mail address'), );
$form['submit'] = array( '#type' => 'submit', '#value' => t('Save'), ); }
return $form; }
Cycle data into next step: form_state Pass data from one step to the next
Finally, a purpose for $form_state in form definition signature! Use $form_state['storage'] in submit handler for signals, data
function newsletter_subscribe_form_submit($form_state) { // signal that the next form building shows confirmation message. $form_state['storage']['confirm'] = 1;
// Pass the email address (assume validated) back to form build. $form_state['storage']['email'] = $form_state['values']['email'];
// trigger a form rebuild. $form_state['rebuild'] = TRUE;}
Signal rebuild of form or redirect in submit handler $form_state['rebuild'] = TRUE; Optional redirect on final step
Drupal 7 Changes drupal_get_form returns structured array, not HTML
Specify text format in textarea and textfield elements
Attach CSS & JS to forms
Javascript element dependency (#557272)$form['element']['#dependencies'] => array(
'enabled' => array( '#edit-menu-has-menu' => array('checked' => TRUE) )),
Use '#markup' not '#value' for elements markup & item (#252013)
drupal_execute => drupal_form_submit (#408024)
Easier to check for node form in hook_form_alter (#161301)
Changes to element definitions re: theming (#355236)
hook_elements -> hook_element_info (#572932) + hook_element_info_alter
D7: drupal_get_form (#353069) drupal_get_form returns form definition array, NOT HTML
D6: $html = drupal_get_form('mymodule_form');
D7: $arrayForm = drupal_get_form('mymodule_form');
Array structure should be passed back to menu handler
All menu handlers should return arrays, not HTML
Arrays are passed through new page building stack
See hook_page_build for rendering
Do not do this!$html = drupal_render(drupal_get_form('mymodule_form'));
Can now modify form structure in hook_page_alter but use hook_form_alter just as you do in D6
D7: Specify text format (#125315) Specify text format for either textfield or textarea elements $form['comment'] = array( '#type' => 'textarea', '#title' => t('Comment'), // Value from {filter_format}.format or filter_fallback_format() etc. '#text_format' => filter_default_format(), );
Form structure is split into value and format sub-arrays // Stores the actual textarea element in subarray
$form['comment']['value']
// Create format picker using filter_form(...) with default $form['comment']['format']
Submit handler retains split between value and format // Unaltered form element value $comment_body = $form_state['values']['comment'];
// Text format (original value specified in #text_format field) $comment_format = $form_state['values']['comment_format']
D7: Attach CSS & JS (#323112) Attached CSS and Javascript directly in form definition
function mymodule_form() { $form = array();
// Attach a css file. $form['#attached']['css'] = array( drupal_get_path('module', mymodule') . '/mymodule.css' );
// Attach Javascript 'file', 'inline', 'external' or 'setting' $form['#attached']['js'] = array( // Inline Javascript. 'alert("Hello World.");' => array('type' => 'inline'),
);
return $form;}
Resources API Reference
http://api.drupal.org/api/drupal/developer--topics--forms_api_reference.html/6
Quick start guide http://api.drupal.org/api/drupal/developer--topics--forms_api.html/6
Tree and parents Tree and parents: http://drupal.org/node/48643
Upgrading modules to D7 Upgrading to D7: http://drupal.org/update/modules/6/7
Maybe useful contributed modules form_builder: Nate Haug's GUI-based form layout tool Webform: one-off forms such as surveys created as nodes Multistep: add multistep forms to CCK Formblock: forms in blocks Formfilter: form editing/filter including formfilter_filter_form API MerlinofChaos ctools: multistep, AJAX (rumor moving into D7 core?) skip_validation (may provide solution for Cancel problem) Fieldset_helper: Saves the collapsed state of a collapsible fieldset
Appendix
AHAH “Asynchronous HTML and HTTP” : Javascript-based dynamic
behavior similar to AJAX
“… the response from the request is used directly without parsing on the clientside”
See poll & upload modules for core samples
$form['choice_wrapper']['poll_more'] = array( '#type' => 'submit', '#value' => t('More choices'), '#description' => t(“click here to add more choices."), '#weight' => 1, '#submit' => array('poll_more_choices_submit'), // If no javascript action. '#ahah' => array( 'event' => 'click', 'path' => 'mymodule/js', 'wrapper' => 'myform-wrapper', 'method' => 'replace', 'effect' => 'fade', 'progress' => array( 'type' => 'bar', 'message' => t('Loading...') )
http://drupalsn.com/learn-drupal/drupal-tutorials/getting-going-ahah-and-drupal-6
drupal_execute// See user.module, line 2387.
function user_register() {
...
}
// http://api.drupal.org/api/function/drupal_execute/6
// register a new user
$form_state = array();
$form_state['values']['name'] = 'robo-user';
$form_state['values']['mail'] = '[email protected]';
$form_state['values']['pass'] = 'password';
$form_state['values']['op'] = t('Create new account');
drupal_execute('user_register', $form_state);
http://drupal.org/node/165104