0

On a Drupal 8 site I have a world map where the user can click on a country; a view with content is then loaded with AJAX using a custom module and the following JavaScript.

(function($, Drupal, drupalSettings, window, document) {
  Drupal.behaviors.worldmap = {
    attach(context, settings) {
      console.log('behaviors called');
      $('#map-container', context).once('addmap').each( () => {
        let map = new InteractiveMap('.interactive-map', (country) => {
          let id = settings.worldmap.yearsnids[country];
          if(!id) {
            id = 0;
          }
          console.log('id');
          let ajax_settings = {
            url: '/my_module/ajax/expand/' +
                  settings.worldmap.ctype + "-infobox/" + id,
          };
          Drupal.ajax(ajax_settings).execute();
        });
        map.init();
      });
...

I have a custom template for the view where I add a ajax link.

<a href="/my_module/ajax/expand/{{ fields.field_content_type.content|render|striptags|trim }}/{{ fields.nid.content|render|striptags|trim }}" class="button use-ajax">See the project</a>

Clicking that link loads another view with more content. So basically a preview is loaded when the user clicks on a country, and a link in the preview then loads the full content if clicked.

Both the preview and the content (also a view), is loaded from my_module with an AjaxResponse:

namespace Drupal\my_module\Controller;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Controller\ControllerBase;

class my_moduleController extends ControllerBase {
  public function loadContent($method, $action, $type, $nid) {
    $response = new AjaxResponse();
    // logic to determine which action and type we want to load
     ...
    $build['view'] = [
      '#type' => 'view',
      '#name' => $viewname,
      '#display_id' => $did,
      '#arguments' => [$nid],
    ];
    $response->addCommand(new ReplaceCommand($replaceClass, 
                          $build, ['fadeIn']));
    return $response;
  }
}

This all works, but the link in the preview only works for the first preview loaded. In every other preview loaded, clicking the link sends the user to the actual ajax-path (eg. mysite.com/my_module/ajax/expand/my_ctype-infobox/the_actual_id), and the content that should have been rendered to the page are displayed like json in a textbox:

[{"command":"settings","settings":{"pluralDelimiter":"\u0003","ajaxPageState":
...

When the page with the map is loaded, and first time the preview is loaded, 'behaviors called' prints to the console - the link to load the full content works. On subsequent times, clicking on a country loads the preview, but behaviors aren't called at all. 'id' gets printed to the console every time I click.

This is the HTML structure:

<div class="interactive-map">
   <div id="map-container"></div>
   <div class="info-box">
     <div class="info-box__content">
        <div class="placeholder"></div>
    </div>
  </div>
</div>
<div class="full-content-wrapper">
  <div class="placeholder"></div>
</div>

The preview is loaded into the info-box__content div, which is outside the #map-container.

1

Your "once()" function is called on an element with an ID. If your HTML is valid there can be only one such element in the DOM.

What happens with the once() function is that it adds a class to the element which marks it as processed. If the once() function is called again on the same element, it will do nothing. So looking at your code, it does indeed seem it will call the each() function just once on first loading, then never again.

It is right to use the once() function in this situation but you probably want to use a selector representing the children that can be dynamically created. I can't really advise what that should be without seeing the actual HTML.

  • Added the HTML to my question. I used each() for lack of something else to put a function into. – Anders Iversen Aug 22 '18 at 10:17
  • Just looked again at your code and am slightly confused as it seems to be calling Drupal.ajax() in response to the DOM load/update rather than on a click() event. Maybe I am missing something. – Alfred Armstrong Aug 22 '18 at 10:52
  • The javascript for the map is in a seperate file. InteractiveMap takes a selector and a function as arguments. The function is called when clicking on a country in the map. – Anders Iversen Aug 22 '18 at 11:00
  • OK that makes more sense now. Seems like the issue then is that the link being clicked on is either not handled by the Drupal Ajax script or somehow the default event handling is executing (event.preventDefault not being called). – Alfred Armstrong Aug 22 '18 at 11:09
  • OK, I think my original point stands in that case but you might also want to look at what happens to your old interactivemap object when you successfully create a new one. There might be the potential for a memory leak. – Alfred Armstrong Aug 22 '18 at 11:21
0

The Root of the problem is the ReplaceCommand in the ajaxResponse.

By reading the issue that I mentioned in my comment, I realized the root of my problem is probably the same as in that issue.

When the preview is loaded the first time by clicking a country, all behaviors are attached correctly to the new div inside .info-box__content.

The next time a preview is loaded, that new div inside .info-box__content is replaced by another via the ReplaceCommand. Drupal.behaviors.worldmap tries to attach the behaviors again, but, for reasons I don't quite know, it tries to attach it to the old div that was just removed by the ReplaceCommand, and thus it fails (silently).

At least this is what I think is going on. To work around that issue, I simply remove any old div and add a new placeholder before the ajax execute call - my javascript now looks like this:

(function($, Drupal, drupalSettings, window, document) {
  Drupal.behaviors.worldmap = {
    attach(context, settings) {
      console.log('behaviors called');
      $('#map-container', context).once('addmap').each( () => {
        let map = new InteractiveMap('.interactive-map', (country) => {
          let id = settings.worldmap.yearsnids[country];
          if(!id) {
            id = 0;
          }
          console.log('id');
          let ajax_settings = {
            url: '/my_module/ajax/expand/' +
                  settings.worldmap.ctype + "-infobox/" + id,
          };
          // new code for removing old div and adding new placeholder 
          $('.info-box__content div', context).remove();
          $('.info-box__content', context).html('<div class="placeholder"></div>');
          Drupal.ajax(ajax_settings).execute();
        });
        map.init();
      });
...

And now it works :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.