Open links in popups with Foundation
James Williams
Let me take you on a journey. We'll pass by Drupal content renderer services, AJAX commands, javascript libraries and a popular front-end framework. If you've only heard of one or two of those things, come lean on the experience I took diving deep into Drupal. I'm pleased with where my adventure took me to, and maybe what I learned will be useful to you too.
Here's the end result: a contact form, launched from a button link in the site header, with the page beneath obscured by an overlay. The form allows site visitors to get in touch from any page, without leaving what they were looking at.
Drupal has its own API for making links launch dialogs (leveraging jQuery UI Dialogs). But our front-end of the site was built with Foundation, the super-popular theming framework, which provides components of its own that are much better for styling. We often base our bespoke themes on Foundation, and manipulate Drupal to fit.
We had already done some styling of Foundation's Reveal component. In those places, the markup to show in the popup is already in the page, but I didn't really want the form to be in the page until it was needed. Instead, AJAX could fetch it in. So I wondered if I could combine Drupal's AJAX APIs with Foundation's Reveal markup and styling. Come with me down the rabbit hole...
There are quite a few components in making this possible. Here's a diagram:
So it comes down to the following parts, which we'll explore together. Wherever custom code is needed, I've posted it in full later in this article.
- A link that uses AJAX, with a dialog type set in an attribute.
- Drupal builds the content of the page that was linked to.
- Drupal's content view subscriber picks up that response and looks for a content renderer service that matches the dialog type.
- The content renderer returns an AJAX command PHP class in its response, and attaches a javascript library that will contain a javascript AJAX command (a method).
- That command returns the content to show in the popup, and that javascript method name.
- The javascript method launches the popup containing the HTML content.
Let's start at the beginning: the link. Drupal's AJAX API for links is pretty neat. We trigger it with two things:
-
A
use-ajax
class, which tells it to open that link via an AJAX call, returning just the main page content (e.g. without headers & footers), to be presented in your existing page. -
A
data-dialog-type
attribute, to instruct how that content should be presented. This can be used for the jQuery UI dialogs (written up elsewhere) or the newer off-canvas sidebar, for example.
I wanted to have a go at creating my own 'dialog type', which would be a Foundation Reveal popup. The HTML fetched by the AJAX call would be shown in it. Let's start with the basic markup I wanted to my link to have:
<a href="/contact" class="use-ajax" data-dialog-type="reveal">Enquire</a>
This could either just be part of content, or I could get this into a template using a preprocess function that would build the link. Something like this:
<?php
// $url could have come from $node->toUrl(), Url::fromRoute() or similar.
// For this example, it's come from a contact form entity.
$url->setOption('attributes', [
'class' => [
'use-ajax',
],
// This attribute tells it to use our kind of dialog
'data-dialog-type' => 'reveal',
]);
// The variable 'popup_launcher' is to be used in the template.
$variables['popup_launcher'] = \Drupal\Core\Link::fromTextAndUrl(t('Enquire'), $url);
After much reading around and breakpoint debugging to figure it out, I discovered that dialog types are matched up to content rendering services. So I needed to define a new one of those, which I could base closely on Drupal's own DialogRenderer
. Here's the definition from my module's mymodule.services.yml
file:
services:
main_content_renderer.foundation_reveal:
class: Drupal\mymodule\Render\MainContent\FoundationReveal
arguments: ['@title_resolver']
tags:
- { name: render.main_content_renderer, format: drupal_reveal }
Adding the tag named 'render.main_content_renderer' means my class will be picked up by core's MainContentRenderersPass when building the container. Drupal's MainContentViewSubscriber
will then consider it as a service that can render responses.
The 'format' part of the tag needs to be the value that our data-dialog-type
attribute has, with (somewhat arbitrarily?) 'drupal_
' prepended. The arguments will just be whatever the constructor for the class needs. I often write my class first and then go back to adjust the service definition once I know what it needs. But I'll be a good tour guide and show you things in order, rather than shuttling you backwards and forwards!
Onto that FoundationReveal
service class now. I started out with a copy of core's own ModalRenderer
which is a simple extension to the DialogRenderer
class. Ultimately, that renderer is geared around returning an AJAX command (see the AJAX API documentation), which comes down to specifying a command to invoke in the client-side javascript with some parameters.
I would need my own command, and my FoundationReveal
renderer would need to specify it to be used. That only two functional differences were needed in comparison to core's DialogRenderer
:
-
Attach a custom library, which would contain the actual javascript command to be invoked:
$main_content['#attached']['library'][] = 'mymodule/dialog.ajax';
-
Return an AJAX command class, that will specify that javascript command (rather than the
OpenDialogCommand
command thatDialogRenderer
uses) - i.e. adding this to the returned$response
:new OpenFoundationRevealCommand('#mymodule-reveal')
We'll learn about that command class later!
So the renderer file, mymodule/src/Render/MainContent/FoundationReveal.php
(in that location in order to match the namespace in the service file definition), looks like this - look out for those two tweaks:
<?php
namespace Drupal\mymodule\Render\MainContent;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Render\MainContent\DialogRenderer;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\mymodule\Ajax\OpenFoundationRevealCommand;
use Symfony\Component\HttpFoundation\Request;
/**
* Default main content renderer for foundation reveal requests.
*/
class FoundationReveal extends DialogRenderer {
/**
* {@inheritdoc}
*/
public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) {
$response = new AjaxResponse();
// First render the main content, because it might provide a title.
$content = drupal_render_root($main_content);
// Attach the library necessary for using the OpenFoundationRevealCommand
// and set the attachments for this Ajax response.
$main_content['#attached']['library'][] = 'core/drupal.dialog.ajax';
$main_content['#attached']['library'][] = 'mymodule/dialog.ajax';
$response->setAttachments($main_content['#attached']);
// Determine the title: use the title provided by the main content if any,
// otherwise get it from the routing information.
$title = isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject());
// Determine the dialog options and the target for the OpenDialogCommand.
$options = $request->request->get('dialogOptions', []);
$response->addCommand(new OpenFoundationRevealCommand('#mymodule-reveal', $title, $content, $options));
return $response;
}
}
That AJAX command class, OpenFoundationRevealCommand
sits in mymodule/src/Ajax/OpenFoundationRevealCommand.php
. Its render()
method is the key, it returns the command which will map to a javascript function, and the actual HTML under 'data'. Here's the code:
<?php
namespace Drupal\mymodule\Ajax;
use Drupal\Core\Ajax\OpenDialogCommand;
use Drupal\Core\StringTranslation\StringTranslationTrait;
/**
* Defines an AJAX command to open certain content in a foundation reveal popup.
*
* @ingroup ajax
*/
class OpenFoundationRevealCommand extends OpenDialogCommand {
use StringTranslationTrait;
/**
* Implements \Drupal\Core\Ajax\CommandInterface:render().
*/
public function render() {
return [
'command' => 'openFoundationReveal',
'selector' => $this->selector,
'settings' => $this->settings,
'data' => $this->getRenderedContent(),
'dialogOptions' => $this->dialogOptions,
];
}
/**
* {@inheritdoc}
*/
protected function getRenderedContent() {
if (empty($this->dialogOptions['title'])) {
$title = '';
}
else {
$title = '<h2 id="reveal-header">' . $this->dialogOptions['title'] . '</h2>';
}
$button = '<button class="close-button" data-close aria-label="' . $this->t('Close') . '" type="button"><span aria-hidden="true">×</span></button>';
return '<div class="reveal-inner clearfix">' . $title . parent::getRenderedContent() . '</div>' . $button;
}
}
Now, I've mentioned that the command needs to match a javascript function. That means adding some new javascript to the page, which, in Drupal 8, we do by defining a library. My 'mymodule/dialog.ajax
' library was attached in the middle of FoundationReveal
above. My library file defines what actual javascript file to include - it is mymodule.libraries.yml
and looks like this:
dialog.ajax:
version: VERSION
js:
js/dialog.ajax.js: {}
dependencies:
- core/drupal.dialog.ajax
Then here's that actual mymodule/js/dialog.ajax.js
file. It adds the 'openFoundationReveal
' method to the prototype of the globally-accessible Drupal.AjaxCommands
. That matches the command name returned by my OpenFoundationRevealCommand::render()
method that we saw.
(function ($, Drupal) {
Drupal.AjaxCommands.prototype.openFoundationReveal = function (ajax, response, status) {
if (!response.selector) {
return false;
}
// An element matching the selector will be added to the page if it does not exist yet.
var $dialog = $(response.selector);
if (!$dialog.length) {
// Foundation expects certain things on a Reveal container.
$dialog = $('<div id="' + response.selector.replace(/^#/, '') + '" class="reveal" aria-labelledby="reveal-header"></div>').appendTo('body');
}
if (!ajax.wrapper) {
ajax.wrapper = $dialog.attr('id');
}
// Get the markup inserted into the page.
response.command = 'insert';
response.method = 'html';
ajax.commands.insert(ajax, response, status);
// The content is ready, now open the dialog!
var popup = new Foundation.Reveal($dialog);
popup.open();
};
})(jQuery, Drupal);
There we have it - that last bit of the command opens the Foundation Reveal popup dialog!
I should also add that since I was showing a contact form in the popup, I installed the Contact ajax module. This meant that a site visitor would stay within the popup once they submit the form, which meant for a clean user experience.
Thanks for following along with me!