Modern "Once" Usage in Custom Drupal Module or Theme JavaScripts (Behaviors)
Using once in Drupal 11 Behaviors
In Drupal 9+, the standalone once library replaced jQuery.once. In Drupal 11 it's the standard approach.
Basic Behavior Structure with once
(function (Drupal, once) {
'use strict';
Drupal.behaviors.myBehavior = {
attach(context, settings) {
// once(id, selector, context) — returns only NEW/unprocessed elements
once('myBehavior', '.my-selector', context).forEach((element) => {
// This runs only once per element, even if attach() is called multiple times
element.addEventListener('click', () => {
console.log('clicked', element);
});
});
},
detach(context, settings, trigger) {
// Clean up when elements are removed (trigger === 'unload')
if (trigger === 'unload') {
once.remove('myBehavior', '.my-selector', context).forEach((element) => {
// remove listeners, reset state, etc.
});
}
}
};
}(Drupal, once));
Key points:
- Pass
onceas an IIFE parameter — don't rely on a global once(id, selector, context)returns an array of elements not yet processed with that ID- The
idmust be unique per behavior to avoid collisions - Always pass
context— never query the entiredocumentunless intentional
Declaring Dependencies in *.libraries.yml
# mymodule.libraries.yml
my-library:
version: VERSION
js:
js/my-script.js: {}
dependencies:
- core/drupal # Drupal.behaviors, Drupal.t(), etc.
- core/once # the once() function
- core/drupalSettings # access to drupalSettings object in JS
# If you need jQuery (avoid if possible in Drupal 11):
my-library-with-jquery:
js:
js/my-jquery-script.js: {}
dependencies:
- core/drupal
- core/once
- core/jquery
Common Core Libraries Reference
Dependency | Provides |
|---|---|
|
|
|
|
|
|
|
|
| Drupal AJAX API |
|
|
Attaching the Library
In a module (hook_page_attachments or render array):
// In a render array
$build['#attached']['library'][] = 'mymodule/my-library';
// With settings passed to JS
$build['#attached']['drupalSettings']['mymodule']['myKey'] = 'myValue';
In a theme (.theme file or hook_page_attachments):
function mytheme_page_attachments_alter(array &$attachments) {
$attachments['#attached']['library'][] = 'mytheme/my-library';
}
In a Twig template:
{{ attach_library('mymodule/my-library') }}
Accessing drupalSettings in JS
(function (Drupal, once, drupalSettings) {
Drupal.behaviors.myBehavior = {
attach(context, settings) {
// 'settings' param mirrors drupalSettings at attach time
const myValue = settings.mymodule?.myKey;
// or directly:
const myValue2 = drupalSettings.mymodule?.myKey;
once('myBehavior', '.my-element', context).forEach((el) => {
el.dataset.value = myValue;
});
}
};
}(Drupal, once, drupalSettings));
Common Mistakes to Avoid
// ❌ Wrong — no context, processes whole document every attach
once('thing', '.item').forEach(...);
// ✅ Correct — scoped to context
once('thing', '.item', context).forEach(...);
// ❌ Wrong — jQuery.once() is removed in Drupal 9+
$('.item').once('thing').each(...);
// ❌ Wrong — relying on global `once` without declaring dependency
// (works by accident but breaks if library order changes)
// ✅ Correct — always pass once as IIFE param and declare core/once
(function(Drupal, once) { ... }(Drupal, once));
The once library marks processed elements with a data-once="id" attribute internally, so even if attach() fires multiple times (AJAX, BigPipe, etc.), your code only runs once per element.
Recent Content
-
2 hours 56 minutes ago
-
1 day ago
-
1 day 1 hour ago
-
1 day 1 hour ago
-
1 day 2 hours ago
-
3 days 7 hours ago
-
3 days 7 hours ago
-
3 days 20 hours ago
-
2 weeks 1 day ago
-
2 weeks 1 day ago
Drupal Topics
Drupal Core APIs(38) Projects(14) Cache API(3) Field API(1) Drupal Security(1) Layout API(1) Form API(1) Entity API(1) Access Policy API(1)