Third & GroveThird & Grove
Jun 12, 2017 - Ross Keatinge

Rendering a View with Ajax in Drupal 8

I recently worked on a Drupal 7 site which presented some content as a typical view of teasers in a grid. When the user clicks on one of the teasers, the full content appears as a modal type overlay on top of the grid. The full content is another view, rendered using Ajax.

I decided to see how to do this in Drupal 8. The key thing here is how to render a view via Ajax when a client side event such a click occurs. Perhaps I didn’t search well enough but I didn’t find clear documentation on how to do this

A very simple way which I’ll mention first is to access a page view display with links which have the use-ajax CSS class. This works well if a jQuery UI dialog or modal serves your needs but if you want full control, read on. I figured that a good way to learn would be to setup a simple view, set it to “Use AJAX = Yes” and see what the paging links do because they’re basically doing what I what to do.

The code that makes this happen is the attachPagerLinkAjax function in core/modules/views/js/ajax_view.js. It involves constructing a Javascript object which specifies the view, display, arguments, triggering element, event and the container for the result. This is submitted to Drupal.ajax(). That attaches the event to the element. We need to do something similar in our own code. The Ajax framework in Drupal does the magic when the element is clicked or whatever the event is.

It’s easy to show a specific example so I will illustrate this using article nodes, a view to display teasers and an Ajax action to display a single article in a different view. Rendering the single article view via Ajax is the key thing here. Everything else is really just creating a list of elements to trigger the Ajax action. They could just as easily be hard coded links.

I used the devel_generate module to create a collection of standard Article nodes.

Create the view with a page display to display the teasers. There is nothing special about this. I showed Content in a Grid with view mode set to Teaser. I made some adjustments at “Manage Display” for the Article content type to trim the body to something smaller.

Create the view to show the selected single article via Ajax. I created another display in the same view but it doesn’t have to be. I created an “Embed” display showing the default view mode in an unformatted list. This display has a contextual filter by content ID. The Ajax request will supply the node id as an argument to determine which article to display.

Create the beginnings (.info and .module files) of a custom module. We’re going to need it to create a custom block. See here if you’re not familiar with how to create a module. My module is called ajax_view_demo so I will use that name in the text below but substitute your own module name. File paths are relative to the root of the module.

We’re going to need some custom Javascript and CSS and specify dependencies. A library is the way to do this in Drupal 8 so put the following in a file ajax_view_demo.libraries.yml.

demo:
  js:
    js/demo.js: {}
  css:
    module:
      css/demo.css: {}
  dependencies:
    - core/drupal
    - core/jquery
    - core/jquery.once
    - core/drupal.ajax
    - core/views.ajax

We need a template to provide the container for the ajax response. Put the following in templates/ajax-view-demo.html.twig.

<div class="js-view-dom-id-ajax-demo"></div>

We need to implement hook_theme to tell Drupal about our template so put this in the .module file.

/**
 * Implements hook_theme()
 */
function ajax_view_demo_theme($existing, $type, $theme, $path) {
  return [
    'ajax_view_demo' => [],
  ];
}

A block seems like the most convenient way to get our markup onto the page so we need to create a block plugin. Put this in the file src/Plugin/Block/AjaxViewBlock.php

<?php
 
namespace Drupal\ajax_view_demo\Plugin\Block;
 
use Drupal\Core\Block\BlockBase;
 
/**
 * Provides a 'AjaxViewBlock' block.
 *
 * @Block(
 *  id = "ajax_view_block",
 *  admin_label = @Translation("Ajax view block"),
 * )
 */
class AjaxViewBlock extends BlockBase {
 
  /**
   * {@inheritdoc}
   */
  public function build() {
 
    $build = [];
 
    $build['ajax_view_block'] = [
      '#theme' => 'ajax_view_demo',
      '#attached' => ['library' => 'ajax_view_demo/demo'],
    ];
 
    return $build;
  }
 
}

This simply renders our template and attaches our Javascript and CSS. Drupal Console is a nice way to quickly generate boilerplate code for modules, blocks etc.

And now some simple CSS in css/demo.css.

.views-col {
    cursor: pointer;
}
 
.views-col :hover {
    background-color: #eeeeee;
}
 
.js-view-dom-id-ajax-demo {
    background-color: #eeeeee;
}

Finally, what this is really all about, the Javascript. Put this in js/demo.js.

(function ($) {
  Drupal.behaviors.ajaxViewDemo = {
    attach: function (context, settings) {
      // Attach ajax action click event of each view column.
      $('.view-articles .views-col').once('attach-links').each(this.attachLink);
    },
 
    attachLink: function (idx, column) {
 
      // Dig out the node id from the header link.
      var link = $(column).find('header a');
      var href = $(link).attr('href');
      var matches = /node\/(\d*)/.exec(href);
      var nid = matches[1];
 
      // Everything we need to specify about the view.
      var view_info = {
        view_name: 'articles',
        view_display_id: 'embed_1',
        view_args: nid,
        view_dom_id: 'ajax-demo'
      };
 
      // Details of the ajax action.
      var ajax_settings = {
        submit: view_info,
        url: '/views/ajax',
        element: column,
        event: 'click'
      };
 
      Drupal.ajax(ajax_settings);
    }
  };
})(jQuery);

The attachLink function is called for every views column. In a view rendered as a grid, “column” really means “cell”.

Note the use of the “once” function. This ensures that the Ajax event is attached only once on the page load and not on every Ajax request. If you see multiple throbbers when you click then that indicates a problem related to this.

We need to know the node id to supply as a view argument for the contextual filter. I’m cheating a little here by digging it out of the title link which is available on the teaser view. This won’t work if the nodes have url aliases or you don’t want to have the title as a link. A better way would be to implement a preprocess function to add a data attribute such as data-nid to the column element and access that with jQuery’s .data() function. The Ajax code takes care of e.preventDefault() on the title link.

view_name, view_display_id and view_args are pretty much self explanatory. If there is more than one argument, separate them with / just as they would be in the url of a page display.

The selector for the container which the Ajax result will fill is set with the view_dom_id parameter. Despite the “id” in the name, the selector is for a class of js-view-dom-id- plus the value of view_dom_id so in my case it is .js-view-dom-id-ajax-demo.

Enable the module, place the block in the content area above the main content.

The demo looks like this.

Ajax demo before click.

Click on the third article and the second view will appear without a page refresh. Appropriate styling could make that appear however you want.

Ajax demo after click.

That’s about it. Just remember that this is a simple arbitrary example. You’re rendering a view and supplying arguments so therefore the Ajax response can be anything a view can do.