Did you know that you cannot use querySelectorAll()
function on <template>
elements? By that I mean that the following code will in fact not return any results:
const element = Object.assign(document.createElement("template"), {
innerHTML: `
<script src="gist://1234567890/~example.js"></script>
<div>Hello <template>world!</template></div>
`
});
alert(`Elements found: ${element.querySelectorAll('*').length}`);
The same thing can be said about trying to use querySelectorAll()
function on <template>
elements:
const element = Object.assign(document.createElement("template"), {
innerHTML: `
<script src="gist://1234567890/~example.js"></script>
<div>Hello <template>world!</template></div>
`
});
alert(`Found an element: ${element.querySelector('*') != null}`);
This even happens when calling either one of these functions on a non-<template>
element when you want to descendants of a lower level <template>
elements:
const element = Object.assign(document.createElement("div"), {
innerHTML: `
<script src="gist://1234567890/~example.js"></script>
<div>Hello <template><b>world</b>!</template></div>
`
});
const bElement = element.querySelector('b') ?? element.querySelectorAll('b')[0];
alert(`<b> element was found: ${bElement != null}`);
Custom Function – queryElements()
Seeing that the above issue occurs when using querySelector()
and querySelectorAll()
I decided to write a function which acts like querySelectorAll()
, but at the same time also returns the descendants of all <template>
elements:
/**
* Similar to calling `root.querySelectorAll(selector)` except this allows for
* descendants of `<template>` elements to also be returned.
* @param {HTMLElement} root
* Element to start looking under for other elements that match `selector`.
* @param {string} selector
* CSS selector string used to match against elements to be returned. This
* CSS selector only works relative to `root` and nested `<template>`
* elements. A CSS selector such as `"template > *"` would not work because
* `template` would always have to be a leaf node in the selector if it is
* specified.
* @returns {HTMLElement[]}
* Unlike `root.querySelectorAll(selector)` this returns an array of all of
* the descendants of `root` that match `selector`, even the descendants of
* `<template>` elements.
*/
function queryElements(root, selector) {
root = (root.nodeName === 'TEMPLATE' && root.content) || root;
const arr = Array.from(root.querySelectorAll(selector));
let i = 0;
for (const t of root.querySelectorAll('template')) {
// 4 comes from Node.DOCUMENT_POSITION_FOLLOWING
for (let l = arr.length; i < l && !(t.compareDocumentPosition(arr[i]) & 4); i++);
arr.splice.apply(arr, [i, 0].concat(queryElements(t, selector)));
}
return arr;
}
Does it work?
Feel free to try it out yourself by clicking the Execute
button to execute the following example:
function queryElements(root, selector) {
root = (root.nodeName === 'TEMPLATE' && root.content) || root;
const arr = Array.from(root.querySelectorAll(selector));
let i = 0;
for (const t of root.querySelectorAll('template')) {
// 4 comes from Node.DOCUMENT_POSITION_FOLLOWING
for (let l = arr.length; i < l && !(t.compareDocumentPosition(arr[i]) & 4); i++);
arr.splice.apply(arr, [i, 0].concat(queryElements(t, selector)));
}
return arr;
}
// Creates a <TEMPLATE> element with a nested <TEMPLATE> element which contains
// a <B> element under it.
const element = Object.assign(document.createElement("template"), {
innerHTML: `
<script src="gist://1234567890/~example.js"></script>
<div>Hello <template><b>world</b>!</template></div>
`
});
const bElement = queryElements(element, 'b')[0];
alert(`<b> element was found: ${bElement != null}`);
There are still a few issues with this function. One is that if you want to specify template
as part of the selector it will only work as the leaf node of a selector (eg. "div.wrapper > div template"
). Another is that your selector will only work relative to either the specified root
element or a nested <template>
element. Still, this function can be pretty useful if you have to deal with DOM elements that may or not be/contain <template>
elements.
Let me know what you think and as always, happy coding! š