Drupal 10: Opening An Ajax Dialog On Page Load

Drupal has a quick and convenient way of creating ajax dialogs that gives users the ability to embed links in content that open up dialog boxes when clicked. This is a useful way of presenting a bit of content to a user without them navigating away from the page.

I have previously written in detail about creating ajax dialogs in Drupal, and I refer back to that article quite often when the need arises.

The simplest way of creating an ajax dialog is by adding the class "use-ajax" and the data-dialog-type attribute, which can be one of dialog, dialog.off_canvas, dialog.off_canvas_top and modal. Using the "use-ajax" class tells Drupal that this is an ajax link and to intercept the click to perform an ajax request.

<a class="use-ajax" data-dialog-type="dialog" href="/node/1">node in dialog</a>

You can also inject options into the HTML to change some of the options in the dialog. For instance, if we wanted to set the width of the dialog to be 70% of the screen size then we would add the data-dialog-options attribute to the link.

<a class="use-ajax" data-dialog-options="{&quot;width&quot;:&quot;70%&quot;}" data-dialog-type="dialog" href="/node/1">node in dialog</a>

Both of these links will open the page at "/node/1" in a dialog window instead of taking the user to the new page.

I recently had a requirement on a site that needed a dialog to re-open if the page was bookmarked or shared with another user. The page in question had a number of dialogs that presented short form content to the user without them needing to reload the page. By default, there is no state set when opening a dialog so I needed to add extra functionality to the dialog system to provide this feature.

I found that the best way to add this state was by appending the hash value like "#node/123" to the end of the URL. This meant that as the page loaded I could look for this hash value and load the ajax dialog for the user. Unlike query parameters, hash values are ignored by Drupal and so I wouldn't have to worry about filtering them out or causing unintended side effects.

The first step was to add a custom JavaScript library called "mymodule/node_modal" to every page load using the hook_page_attachments() hook.

/**
 * Implements hook_page_attachments().
 */
function mymodule_page_attachments(array &$attachments) {
  $attachments['#attached']['library'][] = 'mymodule/node_modal';
}

The node_modal library has a pretty simple structure. We just want to inject a bit of custom JavaScript code into the page and ensure that the JQuery and Drupal ajax libraries are present on the page as well.

node_modal:
  version: 1.0
  js:
    js/node_modal.js: {}
  dependencies:
    - core/jquery
    - core/drupal.ajax

The JavaScript library contains a bit of complexity, so I'll break this down bit by bit.

The first thing to do is to create a "Drupal.behaviors" area that will contain all of the custom JavaScript.

(function nodeModalControl($, Drupal) {
  'use strict';

  Drupal.behaviors.nodeModalControl = {
    attach(context, settings) {

      // All JavaScript goes here.

    }
  };
})(jQuery, Drupal);

When the page is first loaded we need to go through all the elements on the page that contain the data-dialog-type data attribute and add a click event. This click event will append the path of the link that was clicked to the end of the URL as a hash value.

// Find all modal links on the page and attach a click event to them.
const modalLinks = document.querySelectorAll('[data-dialog-type]');
for (let i = 0; i < modalLinks.length; i++) {
  modalLinks[i].addEventListener('click', function openDialogClick(event){
    // When the link is clicked, add the path to the URL as a hash value.
    history.pushState('', '', `#${this.pathname}`);
    event.preventDefault();
  });
}

With this in place, when a user is on the URL "/node/2" and clicks on a link that looks like this.

<a class="use-ajax" data-dialog-options="{&quot;width&quot;:&quot;70%&quot;}" data-dialog-type="dialog" href="/node/1">node in dialog</a>

The URL will change to "/node/2#/node/1" before the dialog is opened. This gives a convenient way of storing what dialog box is currently open that can be easily sent to other users by just sharing the URL.

Of course, we don't want to keep the hash in the URL so we need a mechanism to remove this once the dialog has been closed. Thankfully, the dialog comes with a number of events that we can use to trigger our own code. When a dialog closes the event "dialog:afterclose" is triggered, which we can then listen to and reset the URL back to its original state.

// When the dialog is closed then remove the hash from the URL.
$(window).on('dialog:afterclose', (e, dialog,$element) => {
  history.pushState("", document.title, window.location.pathname + window.location.search);
});

The parameter "window.location.pathname" property contains the current path of the page and the "window.location.search" property contains any query parameters that might exist on the page. By doing this we preserve any functionality that might depend on query strings being present in the page.

Finally, we need a way of detecting the presence of our hash value on page load and triggering our dialog to appear.

This block of code provides this functionality. Here, we detect the presence of the hash in the URL, extract it, and then use the Drupal.ajax() function to trigger the dialog. The settings we pass to the Drupal.ajax() function here are essentially the same options we use for the original dialog link, just as an array of options.

// Run once the document has loaded.
once('init-once', context === document ? 'html' : context)
  .forEach(function initOnce(doc) {
    if (context.hasOwnProperty('location') === false) {
      // If the context has no location then this is a modal window then
      // we do nothing.
      return;
    }
    if (context.location.hash !== '') {
      // Extract the hash value from the URL.
      const hash = location.hash.substring(1);
      // Create the settings required for the ajax callback.
      var ajaxSettings = {
        'url': `${hash}`,
        'dialogType': 'dialog',
        'dialog': {
          'width': '70%'
        },
      };
      // Create the ajax callback object and execute it.
      var modalAjaxObject = Drupal.ajax(ajaxSettings);
      modalAjaxObject.execute();
    }
  });

Critically important to this is to detect if the current context being addressed to determine if it has a location property. This is because the dialog is loaded every time the page loads, including any ajax events that might be triggered. If the context does not have a location then we are looking at the HTML fragment being loaded inside the ajax dialog and can ignore it. Without this simple check in place the dialog would trigger recursively until the browser ran out of memory.

With this JavaScript in place the user will now be presented with an ajax dialog as they visit the page with a hash link to another item of content in it.

Note that this only works with node links. If you want to allow this for different types of content or custom modal links then you'll need to change the 'url' setting you pass to the Drupal.ajax() function and create some sort of controller that will react to the URLs being passed to it.

One limitation of this approach is that any path can be appended to the URL to force it to load onto the page. Drupal's permissions system is still used here so it's not actually possible to load protected content in this way, but you might see an error message that states "Oops, something went wrong. Check your browser's developer console for more details." if there was an error when loading the page. The Cross-Origin resource sharing (CORS) permissions system prevents arbitrary full URLs from being passed to the hash value as well, although the same error is produced.

In the project I created I solved these issues by using a custom controller to listen to the dialog callbacks, which simplifies the ajax settings a little since the dialog options are set in the controller.

// Run once the document has loaded.
once('init-once', context === document ? 'html' : context)
  .forEach(function initOnce(doc) {
    if (context.hasOwnProperty('location') === false) {
      // If the context has no location then this is a modal window then
      // we do nothing.
      return;
    }
    if (context.location.hash !== '') {
      // Extract the hash value from the URL.
      const hash = location.hash.substring(1);
      // Create the settings required for the ajax callback.
      var ajaxSettings = {
        'url': `/some/ajax/endpoint/${hash}`
      };
      // Create the ajax callback object and execute it.
      var modalAjaxObject = Drupal.ajax(ajaxSettings);
      modalAjaxObject.execute();
    }
  });

Using this mechanism I could control how the ajax request was responded to and could therefore control the validation of the input and what sort of data was returned from the request. This did mean that the ajax dialog links need to point to a different URL, and they don't use the "data-dialog-type" attribute any more since the link would always return an ajax dialog. Instead the links just have a class that can be easily added to generate the dialog link. To avoid any confusion, I abstracted the creation of the link away from the users so that they didn't need to worry about the implementation detail of creating the links.

More in this series

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
18 + 0 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.