once() function javascript jQuery drupal, function reference
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/jqueryIn 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 thisonce()call. This ID is added as adata-onceattribute on the processed elements (e.g.,data-once="my-unique-id"). Whenonce()is called again with the sameid, it checks for this attribute to determine if an element has already been processed.selector(string | NodeList | Array<Element> | Element):
- A CSS selector string (e.g.,
'.my-class','#my-id'). - A
NodeList(fromdocument.querySelectorAll()). - An
Arrayof DOM elements. - A single
Element.
- A CSS selector string (e.g.,
context(Document | Element, optional): The element to use as the context forquerySelectorAll. If omitted,documentis used. In Drupal behaviors,contextis typically thecontextparameter passed to theattachfunction, 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 theidfrom an element'sdata-onceattribute, 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 givenid. It doesn't modify the DOM.once.find([id], [context]): Finds elements that have been processed by a givenid. If noidis provided, it returns all elements with adata-onceattribute.
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 modernonce()returns a native JavaScript array of elements. This means you typically useforEach()instead of.each()when working with the nativeonce(). - Dependencies: For
jQuery.once(), you neededcore/jquery.once. For the modernonce(), you needcore/once.
Best Practices with once() and Drupal Behaviors:
- Always use
once()withinDrupal.behaviors.attachfor 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 thecontextargument toonce(). 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 theidargument to avoid conflicts with other modules or themes. - Avoid global event listeners without
once(): If you attach event listeners directly todocumentorwindowwithoutonce(), those listeners will be re-attached every timeattachis called, potentially leading to memory leaks or multiple event firings. For page-level scripts that truly only need to run once, consider applyingonce()to thehtmlorbodyelement withdocumentas 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
Code Snippet
(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);