Skip to main content

once() function javascript jQuery drupal, function reference

DrupalVIP Support

In web development, particularly within the Drupal ecosystem, the once() function is crucial for ensuring that JavaScript code only executes a specific behavior or action on an element one time, even if the attach function of a Drupal behavior is called multiple times (which often happens with AJAX requests).

Here's a breakdown of once() in the context of JavaScript, jQuery, and Drupal:

1. The Problem once() Solves

Drupal utilizes a concept called Drupal.behaviors. These behaviors are designed to be reapplied to elements when new content is loaded into the DOM via AJAX. For instance, if you have a form that loads new fields dynamically, Drupal.behaviors will re-run to ensure your JavaScript is applied to these new elements.

However, if you're not careful, this can lead to your JavaScript code being executed multiple times on the same element. For example, if you attach a click event listener to a button, and then an AJAX request reloads part of the page containing that button, the click listener might be attached again, resulting in the click event firing multiple times.

once() prevents this by marking elements that have already been processed for a given "once ID."

2. once() in Drupal (Modern JavaScript)

Drupal 9 and later have moved away from relying solely on jQuery's once() and now provide a native JavaScript once() library. This is the recommended way to use it in modern Drupal development.

How to use it:

Declare core/once as a dependency: In your .libraries.yml file, ensure core/once is listed as a dependency for your JavaScript library:

my_module_library:
  js:
    js/my_script.js: {}
  dependencies:
    - core/drupal
    - core/once
    # If you still need jQuery, include it too:
    - core/jquery

In your JavaScript file (within Drupal.behaviors):

(function (Drupal, once) {
  'use strict';

  Drupal.behaviors.myCustomBehavior = {
    attach: function (context, settings) {
      // Select elements that haven't been "onced" yet with the ID 'my-unique-id'.
      // 'context' is typically the part of the DOM that was just loaded or updated.
      // '.my-selector' is your CSS selector for the elements you want to target.
      const elementsToProcess = once('my-unique-id', '.my-selector', context);

      // `elementsToProcess` is an array of DOM elements that haven't been processed
      // with 'my-unique-id' yet.
      elementsToProcess.forEach(function (element) {
        // Your JavaScript code to run only once per element.
        // `element` is a native DOM element, not a jQuery object.
        console.log('Processing element:', element);

        // Example: Add a class
        element.classList.add('processed-by-my-behavior');

        // Example: Attach an event listener
        element.addEventListener('click', function() {
          console.log('Click event fired on a processed element!');
        });

        // If you still need jQuery for something specific within the loop,
        // you can wrap the native element:
        // $(element).css('background-color', 'lightgreen');
      });

      // To process the <body> or <html> element only once per full page load,
      // you can use 'html' or 'body' as the selector and omit context (or pass document).
      // Note: `once` returns an array, so if you expect only one, you might destructure or use .shift()
      const [htmlElement] = once('my-global-script', 'html', document);
      if (htmlElement) {
        // This code will run only once when the page fully loads.
        console.log('Global script initialized on HTML element.');
      }
    }
  };
})(Drupal, once);

Arguments of once(id, selector, [context]):

  • id (string): A unique string identifier for this once() call. This ID is added as a data-once attribute on the processed elements (e.g., data-once="my-unique-id"). When once() is called again with the same id, it checks for this attribute to determine if an element has already been processed.
  • selector (string | NodeList | Array&lt;Element> | Element):
    • A CSS selector string (e.g., '.my-class', '#my-id').
    • A NodeList (from document.querySelectorAll()).
    • An Array of DOM elements.
    • A single Element.
  • context (Document | Element, optional): The element to use as the context for querySelectorAll. If omitted, document is used. In Drupal behaviors, context is typically the context parameter passed to the attach function, as it helps limit the scope of the search to newly added content.

Return Value:

once() always returns an Array of Element objects that have not yet been processed by a once() call with the given id.

Other once methods:

The @drupal/once library also provides:

  • once.remove(id, selector, [context]): Removes the id from an element's data-once attribute, allowing the associated JavaScript to be executed on that element again.
  • once.filter(id, selector, [context]): Filters elements, returning those that have been processed by the given id. It doesn't modify the DOM.
  • once.find([id], [context]): Finds elements that have been processed by a given id. If no id is provided, it returns all elements with a data-once attribute.

3. jQuery.once() (Older Drupal versions / Compatibility)

In Drupal 7 and earlier Drupal 8 versions, jQuery.once() was commonly used. While still supported for backward compatibility in some Drupal 9+ scenarios (if core/jquery.once is included as a dependency), the native once() is preferred.

How it worked (example for older Drupal/jQuery once):

(function ($, Drupal) {
  Drupal.behaviors.myOldjQueryBehavior = {
    attach: function (context, settings) {
      // Find elements with the class 'my-old-selector' within the context.
      // The .once() method marks them and then .each() iterates over them.
      $('.my-old-selector', context).once('my-old-unique-id').each(function () {
        // $(this) refers to the current jQuery object of the element being processed.
        $(this).on('click', function() {
          console.log('Old jQuery click event fired once!');
        });
      });
    }
  };
})(jQuery, Drupal);

Key Differences / Migration:

  • Chainability: jQuery.once() was often chained directly with other jQuery methods (.once().on(), .once().each()).
  • Return Value: jQuery.once() returned a jQuery collection. The modern once() returns a native JavaScript array of elements. This means you typically use forEach() instead of .each() when working with the native once().
  • Dependencies: For jQuery.once(), you needed core/jquery.once. For the modern once(), you need core/once.

Best Practices with once() and Drupal Behaviors:

  • Always use once() within Drupal.behaviors.attach for any DOM manipulation or event binding that should only happen once per element, regardless of how many times the behavior is triggered.
  • Pass context: Always pass the context argument to once(). This ensures that your selectors only operate on the newly added or updated portions of the DOM, improving performance and preventing unnecessary re-processing of elements already present on the page.
  • Choose a unique id: Use a descriptive and unique string for the id argument to avoid conflicts with other modules or themes.
  • Avoid global event listeners without once(): If you attach event listeners directly to document or window without once(), those listeners will be re-attached every time attach is called, potentially leading to memory leaks or multiple event firings. For page-level scripts that truly only need to run once, consider applying once() to the html or body element with document as the context, and checking if the returned array has a length greater than 0.

By effectively using the once() function, you can write robust and performant JavaScript for your Drupal applications that gracefully handles AJAX updates and ensures your code executes exactly when and how you intend.

 

my_module_library:
  js:
    js/my_script.js: {}
  dependencies:
    - core/drupal
    - core/once
    # If you still need jQuery, include it too:
    - core/jquery

 

(function (Drupal, once) {
  'use strict';

  Drupal.behaviors.myCustomBehavior = {
    attach: function (context, settings) {
      // Select elements that haven't been "onced" yet with the ID 'my-unique-id'.
      // 'context' is typically the part of the DOM that was just loaded or updated.
      // '.my-selector' is your CSS selector for the elements you want to target.
      const elementsToProcess = once('my-unique-id', '.my-selector', context);

      // `elementsToProcess` is an array of DOM elements that haven't been processed
      // with 'my-unique-id' yet.
      elementsToProcess.forEach(function (element) {
        // Your JavaScript code to run only once per element.
        // `element` is a native DOM element, not a jQuery object.
        console.log('Processing element:', element);

        // Example: Add a class
        element.classList.add('processed-by-my-behavior');

        // Example: Attach an event listener
        element.addEventListener('click', function() {
          console.log('Click event fired on a processed element!');
        });

        // If you still need jQuery for something specific within the loop,
        // you can wrap the native element:
        // $(element).css('background-color', 'lightgreen');
      });

      // To process the <body> or <html> element only once per full page load,
      // you can use 'html' or 'body' as the selector and omit context (or pass document).
      // Note: `once` returns an array, so if you expect only one, you might destructure or use .shift()
      const [htmlElement] = once('my-global-script', 'html', document);
      if (htmlElement) {
        // This code will run only once when the page fully loads.
        console.log('Global script initialized on HTML element.');
      }
    }
  };
})(Drupal, once);