Skip to content Skip to sidebar Skip to footer

QuerySelector Not Finding Template On HTML Import

I am currently trying to learn how to use web components (without the use of Polymer) using the latest stable Chrome 52 (I have also tried this with the webcomponents.js polyfill o

Solution 1:

In the <script>s inside the imported HTML, don't use document.querySelector(...).

Use:

// while inside the imported HTML, `currentDocument` should be used instead of `document`
var currentDocument = document.currentScript.ownerDocument;
...
// notice the usage of `currentDocument`
var templateInsideImportedHtml = currentDocument.querySelector('#template');

Example (fixing the example in the question):

var currentDocument = document.currentScript.ownerDocument; // <-- added this line

class WZView extends HTMLElement {
    createdCallback () {
        var root = this.createShadowRoot();
        var template = currentDocument.querySelector('#template'); // <-- changed this line
        root.appendChild(document.importNode(template.content, true));
    }
}

Compatibility:

Only IE 11 won't support it. Most browsers (including Edge) implement it, and for IE 10 and below there is a polyfill.


Solution 2:

Update: My original answer is garbage. My problem was that I was trying to obtain the currentScript.ownerDocument from a method inside the class, instead of in a script actively running in the current document (e.g., in an IIFE where I define the class and, hence, where the script would be running alongside of the template). A method may be called from another script, the "currentScript" at that point (i.e., possibly a different document altogether, especially if you're importing from other imports, like I was).

So this is bad:

class Foo extends HTMLElement {
    constructor() {
        const template = document.currentScript.ownerDocument.querySelector("template");
        // do something with `template`
    }

}

and this is better:

(() => {

const _template = document.currentScript.ownerDocument.querySelector("template");

class Foo extends HTMLElement {
    constructor() {
        // do something with `_template`
    }
}

})();

Hopefully that helps someone else who is dumb like me.


Original answer:

I also encountered problems trying to gain access to templates from an import hierarchy of some depth. The currentScript suggestion didn't work for me in this case: in Chrome/Chromium, the currentScript always referred to the first import, but never to any of the deeper imports (as I mentioned in a comment to @acdcjunior's answer), and in Firefox (via polyfill), the currentScript was null.

So what I ended up doing was something similar to @Caranicas's answer. I created a utility function that finds the imported file, call it once outside of the class in an IIFE, and then made it a property of the class, like this:

index.html:

    var _resolveImport = function(file) {
        return (function recur(doc) {
            const imports = doc.querySelectorAll(`link[rel="import"]`);
            return Array.prototype.reduce.call(imports, function(p, c) {
                return p || (
                    ~c.href.indexOf(file)
                        ? c.import
                        : recur(c.import)
                );
            }, null);
        })(document);
    }

src/app.html:

<link rel="import" href="src/component.html">
<template>...</template>
<script>
((global) => {

    const _import = global._resolveImport("src/app.html");

    class App extends HTMLElement {

        static get import() {
            return _import;
        }

        connectedCallback() {
            this.render();
            this.$component = new global.Component();
        }

        render() {
            let template = this.constructor.import.querySelector("template");
            //...
        }

        //...
    }
})(this);
</script>

src/component.html:

<template>...</template>
<script>
((global) => {

    const _import = _resolveImport("src/component.html");

    class Component extends HTMLElement {

        static get import() {
            return _import;
        }

        render() {
             let template = this.constructor.import.querySelector("template");
             //...
        }

        //...
    }
    global.Component = Component;
})(this);
</script>

_resolveImport is expensive, so it's a good idea not to call this more than once for each import, and only for imports that actually need it.


Solution 3:

I ran into the same issue, I kept messing around until I got something that worked.

If you use document.querySelector('link[rel=import]') you can get the current import. Adding .import to that will give you the imported document, which you can then use to query your selector

var template = document.querySelector('link[rel=import]').import.querySelector('#template');

EDIT:

This was brittle, in order to do 2 different imports it was a bit more difficult.

I broke it out into its own function. First you need to get all the imports on the page with querySelectorAll. Then using map you can insert the actual template value into an array, and then a quick filter to remove the null values and you can grab the first and only element and that will be the correct template.

getImportedTemplate() {

    const imports = document.querySelectorAll('link[rel=import]');

    return Array.from(imports).map( (link) => {
        return link.import.querySelector('#myTemplate');
    }).filter( (val) => {
        return val !== null;
    })[0];
}

createdCallback() {
    var imported = this.getImportedTemplate();
    var content = imported.content;
    this.appendChild(document.importNode(content, true));
}

Note:

I could have used filter as the only array operation, in lieu of map, but that would only only give me an array with the link in it, so I would have have to either have another variable to catch it in that filter operation, or run querySelector again.


Solution 4:

With polyfilled HTML imports (npm @webcomponents/html-imports ^1.2), the component <template> ends being placed somewhere in the main document header. With native HTML imports, it ends being placed in a separate document. A reliable way to find the template in both cases is this:

[my-component.html]
<template id="my-component">
  ...

<script>
  ...
  const script = document.currentScript;
  const template = script.ownerDocument.querySelector('template#my-component');
  ...
  customElements.define('my-component', ...);

Assign each template a unique id, for example the component name, to select the correct template in the polyfilled case (the guide might be a little too simple in that regard)


Post a Comment for "QuerySelector Not Finding Template On HTML Import"