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

Advertisement

From our blog

Entity Storage, the Drupal 8 Way

In Drupal 7 the Field API introduced the concept of swappable field storage.

The Drupal 6 to 8 Upgrade Challenge - Part 2

Having concluded the readiness assessment, we turn next to migrating the content and configuration. In reality, there’s little chance that we would migrate anything but the blogs from our old site. For the sake of giving Migrate in Core a workout with real conditions, however, we’re going to upgrade with core’s Migrate Drupal module rather than rebuilding.

The Drupal 6 to 8 Upgrade Challenge - Part 1

Nathaniel Catchpole , the Drupal 8 release maintainer and Tag1 Senior Performance Engineer, suggested that Drupal shops everywhere could support the

DrupalCon Austin

The entertainment industry is not for the faint of heart.

Drupal Watchdog Joins the Linux New Media Family
Drupal Watchdog 6.01 is the first issue published by Linux New Media.

Drupal Watchdog 6.01 is the first issue published by Linux New Media. Come see the Drupal Watchdog team at DrupalCon 2016!

Drupal Watchdog was founded in 2011 by Tag1 Consulting as a resource for the Drupal community to share news and information. Now in its sixth year, Drupal Watchdog is ready to expand to meet the needs of this growing community.

Drupal Watchdog will now be published by Linux New Media, aptly described as the Pulse of Open Source.

Welcome to DrupalCon Barcelona - The Director's Cut

For all you schedule-challenged CEOs – and ADHD coders – this Abbreviated Official Director’s Cut is just what the doctor ordered.

Welcome to DrupalCon - The Barcelona Edition

Did we have fun in Barcelona?
OMG, yes!

Did we eat all the tapas on the menu and wash them down with pitchers of sangria?
Yes indeed!

Recursive Closures and How to Get Rid of Them

This came up while manipulating taxonomy children and their children recursively, so it’s as not far from Drupal as you’d think.