devtools part 2 - multi-step forms in dialog modal
I recently introduced the DevTools module, and today I'm going to show you one of my favourite components, dialog, which builds upon the Dialog module (fear not the 7.x-dev version, it's perfectly stable) and allows you to present single- or multi-state forms in a modal.
First and foremost, a small disclaimer: the dialog component was built for use with custom forms, and may not have expected results with forms from core modules.
The plan
In the example module we're about to write, we're going to create a page with a link that will open a custom form in a dialog, proceed through three form states, and upon successful submission of the last state, update the original link with new text.
Getting started
The first thing we're going to do is download & enable the DevTools and Dialog modules, then (in a custom module) load DevTools, and create a menu callback for the page which will contain a link that'll trigger the modal:
// example.moduleif (module_exists('devtools')) { devtools_load();}/** * Implements hook_menu(). */function example_menu() { $base = array( 'file' => 'example.pages.inc', 'file path' => drupal_get_path('module', 'example'), ); $items['dialog-example'] = array( 'title' => 'Dialog Example', 'page callback' => 'dialog_example_page', 'access callback' => TRUE, ) + $base; return $items;}
As you can see, we're creating a page at the /dialog-example
URI, specifying the dialog_example_page()
function (in example.pages.inc) as the callback, and granting it unrestricted access.
The page callback will look as follows:
// example.pages.inc/** * Menu callback for /dialog-example */function dialog_example_page() { $output = ''; drupal_add_library('dialog', 'dialog'); $trigger_id = 'example-dialog-trigger'; $output .= l(t('Open dialog'), 'dialog-example/form/start/' . $trigger_id, array( 'attributes' => array( 'class' => array( 'use-ajax', 'use-dialog', ), 'id' => $trigger_id, ), )); return $output;}
In the callback we're loading the Dialog library, and then creating a unique (per page) ID for the link that'll trigger the modal to open (this is necessary so that we can easily target the link for HTML replacement on final form submission) and to make the form aware of this ID, we pass it as the last part of the URI.
The /dialog-example/form
URI will be created in our hook_menu()
, and the start
part of the URI specifies the initial form state that should be loaded (more on this later). You could add additional arguments to reference the content you're dealing with, user, or anything else that may be useful to your form. The only remaining tidbits is the assignment of use-ajax
and use-dialog
classes on the link, both of which are required.
On to the hook_menu()
for the addition of our new /dialog-example/form
URI:
// example.moduleif (module_exists('devtools')) { devtools_load();}/** * Implements hook_menu(). */function example_menu() { $base = array( 'file' => 'example.pages.inc', 'file path' => drupal_get_path('module', 'example'), ); $items['dialog-example'] = array( 'title' => 'Dialog Example', 'page callback' => 'dialog_example_page', 'access callback' => TRUE, ) + $base; $items['dialog-example/form'] = array( 'title' => 'Dialog Form', 'page callback' => 'dialog_example_form_ajax_delivery', 'page arguments' => array(2, 3), 'access callback' => TRUE, 'type' => MENU_CALLBACK, ) + $base; return $items;}
The dialog_example_form_ajax_delivery()
function will handle the delivery of the AJAX response, while passing through arguments 2
(form state) and 3
(trigger id) to the form.
dialog_example_form_ajax_delivery()
is defined as follows:
/** * AJAX menu callback for /dialog-example/form * @param [type] $state Requested form state. * @param [type] $trigger_id DOM id of AJAX-triggering element. * @return [type] [description] */function dialog_example_form_ajax_delivery($state, $trigger_id) { $commands = dialog_show_form('dialog_example_form', $state, $trigger_id); ajax_deliver(array('#type' => 'ajax', '#commands' => $commands));}
dialog_show_form()
is, in simplified terms, a wrapper for Drupal's drupal_build_form()
function, and will pass on all arguments following the form id (in our case, dialog_example_form
) to the form builder.
The form
Our $form
will be structured as three top-level state containers (beginning with the state_
prefix), and a series of child elements for each state. The $form_state
will be updated to contain various settings, and finally, $form
and $form_state
will be processed by dialog_form_prepare()
.
/** * The example form itself. * @return [type] [description] */function dialog_example_form($form, &$form_state, $state, $trigger_id) { $form = array(); // start state $form['state_start']['name'] = array( '#title' => t('Name'), '#type' => 'textfield', '#required' => TRUE, ); $form['state_start']['submit'] = array( '#type' => 'submit', '#value' => t('Continue'), ); // second state $form['state_title']['title'] = array( '#title' => t('Title'), '#type' => 'textfield', ); $form['state_title']['submit_checkbox'] = array( '#type' => 'checkbox', '#title' => t('Continue'), ); // third state $form['state_final']['title'] = array( '#title' => t('What step is this?'), '#type' => 'radios', '#options' => array( '1' => t('First'), '2' => t('Second'), '3' => t('Third'), ), ); $form['state_final']['submit'] = array( '#type' => 'submit', '#value' => t('Final submit'), ); // dialog configuration $dialog_common_options = array( 'width' => '700px', 'position' => 'center', ); // state configuration $state_common_options = array( 'close_link' => TRUE, 'close_link_text' => t('Cancel'), 'close_link_class' => array( 'class-one', 'class-two', ), ); $form_state['dialog_settings'] = array( 'current_state' => $state, 'trigger_id' => $trigger_id, 'states' => array( 'start' => array( 'dialog' => array('title' => t('First state')) + $dialog_common_options, 'submit' => array( 'submit' => array( 'element' => &$form['state_start']['submit'], 'next_state' => 'title', ) ), ) + $state_common_options, 'title' => array( 'dialog' => array('title' => t('Second state')) + $dialog_common_options, 'close_link_text' => t('Different Cancel Text'), 'submit' => array( 'submit_checkbox' => array( 'element' => &$form['state_title']['submit_checkbox'], 'next_state' => 'final', ) ), ) + $state_common_options, 'final' => array( 'dialog' => array('title' => t('Third state')) + $dialog_common_options, 'close_link' => FALSE, 'submit' => array( 'submit' => array( 'element' => &$form['state_final']['submit'], ), ), 'submit_autoclose' => TRUE, ) + $state_common_options, ), ); dialog_form_prepare($form, $form_state); return $form;}
The form declares the three top-level state containers and their children as regular FAPI elements. Each state has a child element that will be used to submit that particular state (for example's sake, the second state uses a checkbox).
Finally, we're going to add validation & submit handlers with drupal_set_message()
statements to see how our form submissions are processing:
/** * Validation handler for dialog_example_form(). */function dialog_example_form_validate($form, &$form_state) { $state = $form_state['dialog_settings']['current_state']; drupal_set_message(t('Validate for state: !state', array('!state' => $state)));}/** * Submit handler for dialog_example_form(). */function dialog_example_form_submit($form, &$form_state) { $state = $form_state['dialog_settings']['current_state']; drupal_set_message(t('Submit for state: !state', array('!state' => $state)));}
The dialog settings configured in $form_state
specify each state, its title and an array of options passed to the actual jQuery Dialog plugin. Furthermore, the submit
key specifies an array of elements which trigger the state's form submission. The element
key should contain a reference to the element within $form
, while the optional next_state
key specifies the next form state to load (this could be used to jump to any state); since this key is optional, a single form can be used for multiple "stand-alone" form states.
By default, a 'Cancel' link is appended to each form state, but this can be disabled with the close_link
key, and its text can be changed by using close_link_text
.
Some additional options include: submit_autoclose
, submit_redirect
(should be an absolute URL), and submit_js_callback
, which takes an array with behavior
and method
keys that specify the JS method to call upon successful form submission. For example, the values
'submit_js_callback' => array( 'behavior' => 'example', 'method' => 'form_submit_js_callback',),
will result in Drupal.behaviors.example.form_submit_js_callback()
being called.
If you're interested in seeing the internal details of how DevTools' Dialog component works, have a look at devtools/components/dialog/dialong.inc
.
This wraps up the process of creating multi-step forms in modals with quite a bit of additional functionality. If you have any questions, hit the comments below!
Tagsformmodaldrupaldrupal planetFilesDevTools' Dialog Example Module