0

Moving from Dupal 7 to 9, I'm having a problem with javascript behaviors being called multiple times. In the browser console, when I load a page, I'm seeing:

process test for [object HTMLDocument] pet_guides.js:27:15
process test for [object HTMLUListElement] pet_guides.js:27:15
process test for https://whiskerdocs-local-d9.pglatz.com/portal/config/user-interface/shortcut/manage/default/customize pet_guides.js:27:15
process test for [object HTMLUListElement] 2 pet_guides.js:27:15
process test for [object HTMLElement] 2 pet_guides.js:27:15

​This is my code:

  Drupal.behaviors.guideTest = {
    attach: function (context, settings) {
      console.log('process test for ' + context);
    }
  };

I'm looking at the docs on drupal.org and it isn't making sense to me yet. What I reall want to do is where console.log in to run a loop like

  $.each($('.section-wrapper'), function(index) {
    $(this).addClass('foo');
  });

What is the correct syntax to use a once() to make this execute only one time? If I had a behavior with more than one loop, is there a way to use a single once() wrapper arond them all, or would each look need its own? I thought using a behavior was like using jQuery(document).ready(function ($), so the code would only be executed when the DOM was fully loaded, and don't understand why it is being called for five different contexts.

1 Answer 1

2

I thought using a behavior was like using jQuery(document).ready(function ($), so the code would only be executed when the DOM was fully loaded, and don't understand why it is being called for five different contexts.

No. If it would be like this, behaviors would be pointless.

  1. Behaviors are tied to a single DOM node, because in Drupal you don't know when this DOM node is rendered/injected in the page (remember BigPipe, Ajax Views, Ajax shopping carts, etc).
  2. Behaviors are fired first time after DOM ready, AND potentially very often afterwards when new partials are injected into the DOM. To avoid duplicating event listeners on pre-existing, older nodes use the once-function.
  3. The context just makes it faster to de- and re-attach the behaviors, if you omit the context, any injection of a DOM node has to search the entire document for potential behaviors, and not just inside the context of the newly injected partial.
  4. The equivalent of $(document).ready() would be calling $(document, context).once() inside a behavior, because the document-context is passed only once.

Your loop code would look like this:

Drupal.behaviors.myCustomBehavior= {
  attach: function (context, settings) {
    $('.section-wrapper', context).once('myCustomOnceId').each( function(index) {
      $(this).addClass('foo');
    });
  }
};

So, e.g. if your <div class="section-wrapper"> is inside a row of an infinite scroll view, every <div> will get exactly one foo class, no matter when it is loaded.

Continuing the example above with the infinite scroll view and the "foo" class, after 2 Ajax injections...

...without using a behavior, you would end up with something like
<div class="section-wrapper foo"></div><div class="section-wrapper"></div><div class="section-wrapper"></div>

...using a behavior without once, you would end up with something like
<div class="section-wrapper foo foo foo"></div><div class="section-wrapper foo foo"></div><div class="section-wrapper foo"></div>

1
  • Thanks for the clear explanation and examples.
    – pglatz
    Jan 5, 2022 at 22:50

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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