fromJune 2014
Article:

Make Mine a Modal

The New Dialog and Modal API
1

Image 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:

Model - before.

And when this issue lands, it will look like so:

Model - after.

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 :(