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! šŸ˜Ž

Categories: BlogJavaScript

Leave a Reply

Your email address will not be published. Required fields are marked *