Drupal JavaScript
Overview
Drupal attaches JavaScript libraries to the render arrays it builds - form #states, autocomplete, module behaviours. In a decoupled setup only the rendered markup travels to the frontend, so those attachments would be lost on the way out. To avoid that, the Custom Elements module emits every attached library as a <drupal-library-*> custom element next to the markup, such that a frontend can load the JavaScript and run the Drupal behaviours.
How does it work?
Whenever Drupal converts a render array into a custom element - a Drupal form, a fallback-rendered block, a Canvas component tree - the libraries that bubbled up into the render array's #attached are turned into custom elements:
- Library dependencies are resolved into load order by Drupal, so the frontend can load the files sequentially without knowing anything about Drupal's library graph.
- One element is emitted per library that carries JavaScript, in that order, as a sibling preceding the markup in the same slot. Libraries contributing only CSS or settings are skipped.
- The element name is the library name, lower-cased and with
/,.and_replaced by-:core/drupal.statesbecomesdrupal-library-core-drupal-states. The verbatim library name stays available in thelibraryprop. - Each element carries its library's JavaScript files in the
jsprop, asurl(root-relative, with a cache-busting version query - or absolute for external assets) plus theattributesDrupal would put on the<script>tag. - The merged
drupalSettingsare added to the first library element, JSON-encoded as a string so that setting keys reach the frontend verbatim. - Stylesheets are not emitted: the decoupled frontend brings its own styles.
Example of a form response carrying two libraries:
{
"element": "drupal-form-example-form",
"slots": {
"default": [
{
"element": "drupal-library-core-drupal",
"props": {
"library": "core/drupal",
"js": [{ "url": "/core/misc/drupal.js?v=11.3.13", "attributes": [] }],
"drupalSettings": "{\"ajaxTrustedUrl\":{\"form_action_p_4kMHZg\":true}}"
}
},
{
"element": "drupal-library-core-drupal-states",
"props": {
"library": "core/drupal.states",
"js": [{ "url": "/core/misc/states.js?v=11.3.13", "attributes": [] }]
}
},
"<form class=\"example-form\">...</form>"
]
}
}
Frontend support
Library elements are ordinary custom elements, so a frontend renders them like any other element. The component doing so should:
- Render no markup of its own - the element is a load instruction, not content.
- Resolve the
jsURLs against the Drupal base URL, since they are root-relative. - Add them as
<script>tags in the given order and keep execution order, so that dependencies run before dependents. - Load every URL only once per page, since libraries may share files.
- Merge
drupalSettingsintowindow.drupalSettingsbefore any script runs. A decoupled page has no settings<script>for Drupal's owndrupalSettingsLoader.jsto read, so the frontend takes that over. - Call
Drupal.attachBehaviors(document.body, window.drupalSettings)once the scripts have loaded, so the behaviours attach to the server-rendered markup. - Skip a file that fails to load with a warning instead of aborting: some Drupal "JavaScript" is generated per request and may legitimately be unavailable in a decoupled context.
This is a client-side concern only - there is nothing to do while server-side rendering the page.
Example: Nuxt
The Nuxt connector module ships a loadLibrary() composable doing all of the above, paired with a renderless default component that applies to every library element. See Drupal JS libraries for the Nuxt-specific documentation.
Overriding or skipping a library
Since every library gets a custom element of its own, a frontend can take over a single library by providing a component for that element name, e.g. drupal-library-core-drupal-states for core/drupal.states. On a frontend with a default-component fallback (like Nuxt), that component takes precedence over the generic one, so the library's JavaScript is never loaded. That way a library can be replaced by a native frontend implementation, or skipped entirely by rendering nothing.
Limitations
- No CSS: libraries whose behaviour relies on Drupal core CSS (dialog, off-canvas) work, but are unstyled until the frontend provides its own styles.
- JavaScript-enabled behaviours only: everything Drupal does server-side stays server-side; this only restores what Drupal would have run in the browser.
- Only
#states-style behaviours are verified end-to-end so far. Ajax- and dialog-based libraries are expected to need more work in a decoupled context, since they rely on Drupal's own page URLs and response formats.