In order to allow scripts to remove dependency on jQuery the once feature has been implemented in vanilla js. Additionally it is distributed as a npm package under the @drupal/once namespace.
API Additions
- Declare a new
core/once
library - Deprecate the
core/jquery.once
library - Introduce the
once
global variable to eslint config - A new once library with 4 functions:
once
(equivalent to jQuery.fn.once)once.filter
(equivalent to jQuery.fn.findOnce)once.remove
(equivalent to jQuery.fn.removeOnce)once.find
(new function not previously possible with the jQuery implementation)
Backwards compatibility
If code is relying on an existing once call from core, an additional BC layer is automatically added when once is used alongside jquery.once. Calls to jQuery.once will honor previous calls to once().
// Core calls once()
once('once-id', '[data-drupal-selector="element"]');
// In a contrib or custom code, jQuery.once will work as expected:
$('[data-drupal-selector="element"]').once('once-id') // will return an empty set.
It does not work the other way. Calls to jQuery.once will be ignored by once().
once API
The once functions take 2 parameters, a string id without spaces and an array-like object. The recommended way to use it is to call it with the string you would use in a querySelectorAll()
call. You can only use once with objects that are instances of Element, which means you can not use it with the document
or window
object. If you need a page-level element please use 'html'
or 'body'
as the selector used in the once call: once('my-global-once', 'html')
.
The full documentation is available on npm package page once API
once(id, elements (CSS selector string, Array, NodeList, jQuery), context (optional))
Selects all the elements that have not already been "onced" and add the "id" to the value of the data-once
attribute.
const elements = once('myfeature', '[data-myfeature]');
# changes in the HTML
<div data-myfeature data-once="myfeature">
<div data-myfeature data-once="myfeature another-once-id yet-another-once-id">
once.filter(id, elements, context (optional))
This function filters the elements and returns those for which the once "id" has already been executed.
const filteredElements = once.filter('myfeature', '[data-myfeature]');
once.remove(id, elements, context (optional))
Remove the "id" value from the data-once attribute and returns all elements that matched the once id.
const removedElements = once.remove('myfeature', '[data-myfeature]');
once.find(id (optional), context (optional))
This function query the DOM and return elements for which the once "id" has already been executed, an optional context
parameter can be passed to restrict the search of element to a section of the DOM.
const oncedElements = once.find('myfeature');
// Equivalent to `document.querySelectorAll('[data-once~="myfeature"]');`
const oncedElementsInContext = once.find('myfeature', context);
// Equivalent to `context.querySelectorAll('[data-once~="myfeature"]');`
const allOncedElements = once.find();
// Equivalent to `document.querySelectorAll('[data-once]');`
It is now possible to select elements that have been onced with a css selector such as [data-once~="myfeature"]
to use in CSS files. On the JavaScript side it is easier to use the previous once.find('myfeature')
function.
Code changes needed
Before
# mymodule.libraries.yml
myfeature:
js:
js/myfeature.js: {}
dependencies:
- core/drupal
- core/jquery
- core/jquery.once
# js/myfeature.js
(function ($, Drupal) {
Drupal.behaviors.myfeature = {
attach(context) {
const $elements = $(context).find('[data-myfeature]').once('myfeature');
// `$elements` is always a jQuery object.
$elements.each(processingCallback);
}
};
function processingCallback(index, value) {}
}(jQuery, Drupal));
After (removing jQuery dependency)
# mymodule.libraries.yml
myfeature:
js:
js/myfeature.js: {}
dependencies:
- core/drupal
- core/once
# js/myfeature.js
(function (Drupal, once) {
Drupal.behaviors.myfeature = {
attach(context) {
const elements = once('myfeature', '[data-myfeature]', context);
// `elements` is always an Array.
elements.forEach(processingCallback);
}
};
// The parameters are reversed in the callback between jQuery `.each` method
// and the native `.forEach` array method.
function processingCallback(value, index) {}
}(Drupal, once));
After (keeping the jQuery dependency)
If the code is already using jQuery and removing the dependency is not in scope an alternative form can be use to minimize code changes.
# mymodule.libraries.yml
myfeature:
js:
js/myfeature.js: {}
dependencies:
- core/drupal
- core/jquery
- core/once
# js/myfeature.js
(function (Drupal, $, once) {
Drupal.behaviors.myfeature = {
attach(context) {
// The once call is wrapped in $() to allow the usual jQuery chaining.
const $elements = $(once('myfeature', '[data-myfeature]', context));
// `$elements` is always a jQuery object.
$elements.each(processingCallback);
}
};
function processingCallback(index, value) {}
}(Drupal, jQuery, once));
More code change examples
Before | After (recommended) | After (alternate version) |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
Comments
Additional examples
Additional examples at https://www.drupal.org/project/drupal/issues/3183149
In case of use $('element')
In case of use $('element').once()... where the once is empty but your value comes from another module like data-once('tabsClick')
If I change to $(once('myNewId', 'element')), the data-once renders like this data-once('tabsClick myNewId')
Should I keep the data-once with 2 values or Should I use the once.remove() e then $(once('myNewId', 'element'))?
The default parameter for "id
The default parameter for "id" in the original jQuery Once was "once". So you should probably make your own unique once identifier.
.... And avoid calling the remove() function in general.
a) "Deprecate the core/jquery
a) "Deprecate the core/jquery.once library" means that the previously jQuery version of once was been removed from Drupal 10.
b) Changing to the vanilla javascript once() may require re-writing other code. For example, the new once() stores the status in an attribute, meaning that it persists through a jQuery remove() / append() operation, whereas the original jQuery version does not.
The following regex helped me
The following regex helped me replace a lot of old once() implementations with new one for D10 upgrade:
\$\((.*)\)\.once\(('.*')\)
=>$(once($2, $1))
And the following one for instances where no id was defined. Make sure to replace `custom-id` with something specific later.
\$\((.*)\)\.once\(\)
=>$(once('custom-id', $1))
Thank you very much!
This regex saved us a lot of time. Thanks!
Thanks for the great support!
Thanks for the great support!