7

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 on Chrome 52). However, when I do I seem to get an error with the querySelector. When I attempt to grab the (admittedly poorly named template id) in the console via document.querySelector('#template') it is null and is unable to find it.

I am using this guide albeit with some ES6 syntax. (I also tried direct copy and paste and it had the same issue)

I also tried to search within the shadowDOM, but it didn't exist there either.

view.html

<template id="template">
  <style>
  </style>
  <div class="container">
    <h1>WZView</h1>
  </div>
</template>
<script>
"use strict";

class WZView extends HTMLElement {

  createdCallback () {
    var root = this.createShadowRoot();
    var template = document.querySelector('#template');
    root.appendChild(document.importNode(template.content, true));
  }

}

document.registerElement('wz-view', WZView);
</script>

index.html

<!DOCTYPE html>
<html>
<head>
<!--<script src="/bower_components/webcomponentsjs/webcomponents.js"></script>-->
<link rel="import" href="view.html">
</head>
<body>
  <wz-view></wz-view>
</body>
</html>

console:

view.html:16 Uncaught TypeError: Cannot read property 'content' of null
> document.querySelector('#template')
null
Alyssa Herzog
  • 73
  • 2
  • 3
  • [MDN Template Page](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/template) and [caniuse templates](http://caniuse.com/#feat=template) – Alyssa Herzog Jun 06 '16 at 19:28
  • thanks Alyssa. Learned something new :) Does your code work if you use document.querySelectorAll('#template') ? – yezzz Jun 06 '16 at 19:42
  • Same error as before – Alyssa Herzog Jun 06 '16 at 19:47
  • 1
    If I create a template element in an ordinary web page, I can use the use the querySelector to get the element. Your template element doesn't exist in the document object since it's in an import. Try `document.currentScript.ownerDocument.querySelector('#template')`. – dyagmin Jun 06 '16 at 20:58
  • changing it in the callback, it complains about the ownerDocument object. `Uncaught TypeError: Cannot read property 'ownerDocument' of null` – Alyssa Herzog Jun 07 '16 at 02:16

4 Answers4

4

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.

Community
  • 1
  • 1
acdcjunior
  • 132,397
  • 37
  • 331
  • 304
  • This doesn't seem to work for imports from imports (i.e., document -> import -> import). What I'm seeing for the second import is that the `currentScript` is the same as the first import. This is on Chromium 59 – tjb1982 Jul 23 '17 at 22:53
  • @tjb1982 Have you tested in other browsers? Hard to say if it is a bug or the designed behavior. – acdcjunior Jul 24 '17 at 00:04
  • This doesn't works for me. Getting: Uncaught TypeError: Cannot read property 'ownerDocument' of null – Robert Koszewski Sep 08 '17 at 15:09
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.

tjb1982
  • 2,257
  • 2
  • 26
  • 39
1

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.

Caranicas
  • 149
  • 1
  • 11
0

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)

Markus
  • 668
  • 7
  • 27