Dialogs and Modals are an important UX pattern and can be used effectively both to provide information and to handle user interaction.
A key use for Dialogs and Modals in Drupal is to present a new user interaction without losing the original context. For example, when editing Views settings the modal allows the user to be presented with a new interface without navigating away from their original location.
Displaying Modals in Drupal 7
In Drupal 7, there are a number of approaches and modules for displaying and working with modals and dialogs. Views UI is probably the most common place where sitebuilders interact with modals in Drupal 7, closely followed by Panels/Page Manager. Both of these use modals for simplifying the user interface and the lazy-loading of elements when needed, keeping the interface uncluttered until a specific user interaction is required.
In Drupal 6, there were a number of dialog/modal API modules – with varying popularity – including Modal Frame API, Dialog API, and Popups API, but none have even reached an alpha release for Drupal 7, leaving Ctools Modal as the de facto API for Drupal 7.
Common Use, Different Approach
While each Drupal 6 and 7 modal/dialog module has a common use-case and set of requirements, each implement the functionality in their own way. Additionally, many of these use a Not Invented Here paradigm to roll custom solutions into a problem that’s already been solved in the wider web-community. As a result, many of these solutions are lacking in certain areas, such as accessibility. Also, given the range of different solutions and APIs, DX and consistency suffers.
Drupal 7 already includes the jQuery.UI library which itself contains a Dialog component. The Views modal uses the jQuery.UI Dialog while the Ctools module doesn't – further emphasizing the disconnect in approaches.
With Views coming into core in Drupal 8, we needed a Dialog/Modal API for it to use; this led us to develop the current solution, meaning that core now has an API for this functionality.
In addition, because accessibility is one of the core gates, we needed to solve the problem in a way that didn't exclude screen-reader users, those who prefer a keyboard, and those with JavaScript disabled.
Rather than continue the “not-invented-here” approach, we reached out to the jQuery.UI team and worked with them to solve some accessibility short-comings in the then stable-release. These made it into the jQuery.UI 1.10 release, cross-project collaboration for the win!
Handling non-js Fallbacks
One of the shortcomings of Drupal 7's routing system was that you had to juggle whether the user has JavaScript enabled when serving dialogs/modals. It was common to see URLs containing a nojs
slug. For example, in Views UI there were two versions of each URL for JavaScript and non-JavaScript. The markup would render the URLs with the nojs
form (e.g., 'http://example.com/admin/structure/views/nojs/display/myview/default/style_plugin
' then the JavaScript would handle fetching the content from 'http://example.com/admin/structure/views/ajax/display/myview/default/style_plugin
', with the menu callback at the ajax
path returning Ajax commands to display a modal, and the nojs
returning a normal form via a page callback for those with JavaScript disabled.
Drupal 8's Routing System
Drupal 8's routing system, based on that of Symfony 2, has support for the Accept request header baked into it. This means you can serve two different versions of the same content at any URL depending on the Accept headers used in the incoming request. For example, you could serve an HTML version of a node at node/1 as well as a JSON version, with only the accept-header varying.
This is achieved with a _format entry in your routing requirements entry. For example:
mymodule.route_html: path: '/admin/config/mymodule' defaults: _title: 'My module' _content: '\Drupal\mymodule\Controller\MyModuleController::somePage' requirements: _format: 'html' _access: 'TRUE' mymodule.route_json: path: '/admin/config/mymodule' defaults: _controller: '\Drupal\mymodule\Controller\MyModuleController::jsonCallback' requirements: _format: 'json' _access: 'TRUE'
RouteEnhancers
Another key element in the new Drupal 8 routing system is the concept of RouteEnhancers. These are from the Symfony CMF routing component. They are similar to Drupal 7's hook_menu_alter()
, but because they run at the time of Request instead of when the cache is empty, they have the opportunity to essentially re-route an incoming request.
One such enhancer is the ContentControllerEnhancer
which handles incoming requests for Ajax, HTML, and dialogs/modals. In the case of Ajax requests, it makes sure the response is routed via the AjaxController
. In the case of HTML requests, it sends the request via the HtmlPageController
, which is responsible for wrapping the inner-page content in blocks etc. But the behavior we're interested in here is when it routes incoming requests with an Accept
header of either application/vnd.drupal-modal
or application/vnd.drupal-dialog
to the DialogController
.
The DialogController
This is the guts of the PHP side of the Dialog API. It handles incoming Dialog requests and returns the response in a format that the JavaScript code running client side then uses to display the dialog or modal.
So how does it work? The ContentControllerEnhancer
sends the request via the DialogController
in a manner which allows the DialogController
to ascertain where the original request would have ended up if it were a standard (HTML) page request. The DialogController
then uses this information to get the original content that would have been seen on that page (minus the blocks, etc.) that wrap the inner content on a Drupal page.
The DialogController
then creates the necessary AjaxCommand
objects for displaying the dialog/modal and returns an AjaxResponse
object in a similar fashion to any other AjaxCommand/AjaxResponse
. The JavaScript in the client-side code that made the request then executes these commands and the dialog/modal is displayed.
Using the Dialog API
There are two main ways to use the Dialog API: either with a link, or with a form-button.
Simple Link Example
To make a link return the content in a Dialog, all you need to do is add two attributes; the use-ajax
class and the appropriate data-accepts
attribute, depending on whether you want a modal or a plain dialog. To request a modal, use data-accepts='application/vnd.drupal-modal'
. To request a dialog, use data-accepts='application/vnd.drupal-dialog'
.
<a class="use-ajax" data-accepts="application/vnd.drupal-modal" href="some/path">Make mine a modal</a>
Form Example
To use a form button to trigger a dialog, just setup an #ajax
property like any other Ajax behavior, add an accept behavior and a callback method to return a new AjaxResponse
containing an OpenDialogCommand
or OpenModalDialogCommand
.
<?php /** * {@inheritdoc} */ public function buildForm(array $form, array &$form_state) { // Make the button return results as a modal. $form['foo'] = array( '#type' => 'submit', '#value' => t('Make it a modal!'), '#ajax' => array( 'accepts' => 'application/vnd.drupal-modal', 'callback' => array($this, 'foo'), ), ); return parent::buildForm($form, $form_state); } /** * Ajax callback to display a modal. */ public function foo(array &$form, array &$form_state) { $content = array( 'content' => array( '#markup' => 'My return', ), ); $response = new AjaxResponse(); $html = drupal_render($content); $response->addCommand(new OpenModalDialogCommand('Hi', $html)); return $response; }
The resultant modal looks like so:
And when this issue lands, it will look like so:
Summary
So that's a quick overview of the Dialog API. I'm looking forward to the possibilities this will open up for Drupal 8 contrib. Particularly for themers, the ability to quickly add two attributes to a link and get the result in a modal is going to make adding dynamic interactions far simpler.
One place where this will make a huge UX improvement is for confirmation forms: clicking the 'delete' link for a piece of content could load the confirmation form in a modal, with no need to redirect the user to a new location.
Bring on Drupal 8!
Image: "222/365 - book in bloom" by orangesparrow is licensed under CC BY-NC-ND 2.0
Comments
Such a nice article. So sad it's already pretty outdated :(