Launching an AJAX modal from a WYSWIYG link and customising the response with MainContentRendererInterface in Drupal 8
On a recent project a feature was requested to allow admins to launch some content within a modal window from the WYSWIYG on a Drupal 8 website. The goals were as follows:
- Open any content on the site in a modal window.
- Fall back to a plain link for search engines and non-js based browsers.
- Implement a branded modal design.
This is how we accomplished it.
One of the lesser known features of Drupal core is being able to add a "use-ajax" class to links, which will automatically make an AJAX request for the target page, render just the content part of the page and return the response in a modal window depending some data attributes on the link. The link looks something like:
<a href="/some/page" class="use-ajax" data-dialog-type="modal">A link</a>
In order to save the client from having to insert data attributes into their content, we created a simple filter to replace any instance of a class "modal-trigger" with the ajax class and the correct data attributes. Then by adding the "modal-trigger" class the WYSIWYG styles dropdown, admins could easily choose which links would launch modal windows. The guts of our filter to replace the class looked something like:
$elements = $html_dom->getElementsByTagName('a'); foreach ($elements as $element) { $class = $element->getAttribute('class'); if (preg_match('/modal-trigger/i', $class, $matches)) { $element->setAttribute('class', $class . ' use-ajax'); $element->setAttribute('data-dialog-type', 'modal'); } }
The second problem we encountered was branding the jQuery UI modal window that ships with core by default. Our front-end workflow includes writing markup and styles for components ahead of integrating them into Drupal so the component was already built and styled a modal with a sample markup. When it came to applying these styles to jQuery UI modal, the JavaScript that deals with positioning the content as well as other features like being able to move the window around were conflicting with our styles or simply not needed. To deal with this, we decided we would load the content using AJAX, but append the content to the page and let the CSS do it's thing.
Drupal core has the conept of a "main content renderer". These are tagged services which define different ways in which the primary section of content on a page can be rendered. The renderer most people would see every day is HtmlRenderer which places the content of the page amonst regions and blocks configured in the blocks UI and delivers it as an HTML response. Modal windows in core use this concept to render the main page content, insert it into an AJAX response along with some AJAX commands to open jQuery UI modal.
We created a very simple main content renderer that simply appended the target content to the bottom of the page and allowed the CSS to position the modal, making it more performant and easier to style. The guts of it looks something like:
/** * {@inheritdoc} */ public function renderResponse(array $main_content, Request $request, RouteMatchInterface $route_match) { $response = new AjaxResponse(); $title = isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject()); $main_content['#theme_wrappers']['css_modal'] = [ 'title' => $title, ]; $content = $this->renderer->renderRoot($main_content); $main_content['#attached']['library'][] = 'css_modal/css_modal'; $response->setAttachments($main_content['#attached']); $response->addCommand(new AppendCommand('body', $content)); return $response; }
After tagging the service definition with "render.main_content_renderer", we were then able to update the data-dialog-type attribute to request our new main content renderer and have the styled and markup-wrapped modal be inserted into the bottom of the page.
The result? A flexible, performant and branded modal window for any content on the site.