QuerySelector does not find HTML import template

I'm currently trying to learn how to use web components (without using Polymer) using the latest stable Chrome 52 (I also tried this with webcomponents.js polyfill in Chrome 52). However, when I do this, I seem to get an error with querySelector. When I try to capture (admittedly, a poorly named template identifier) ​​in the console using document.querySelector('#template') , it is null and cannot find it.

I use this guide , albeit with some ES6 syntax. (I also tried direct copy and paste, and it had the same problem)

I also tried searching in 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> 

Prefixes:

 view.html:16 Uncaught TypeError: Cannot read property 'content' of null > document.querySelector('#template') null 
+6
source share
4 answers

In <script> inside imported HTML, do not use document.querySelector(...) .

Using:

 // 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 (correction of an 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 does not support it . Most browsers (including Edge) implement it , and for IE 10 and below there is a polyfill .

+3
source

I ran into the same problem, 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 this will give you an imported document, which you can then use to query your selector

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

EDIT:

It was fragile to make 2 different imports, it was a little harder.

I broke it into my own function. First you need to get all the imports on the page using querySelectorAll . Then, using the map, you can insert the actual value of the template into an array, and then a quick filter to remove the null values, and you can capture 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 use a filter as the only operation with an array, instead of a map, but that would only give me an array with a link in it, so I would either have to have another variable to catch it in this filter or run querySelector again.

+1
source

Update: My initial answer is garbage. My problem was that I was trying to get currentScript.ownerDocument from a method inside the class, and not actively running in the script in the current document (for example, in IIFE, where I define the class, and therefore where the script will work next to the template ) The method can be called from another script, "currentScript" at that moment (i.e., Maybe another document in general, especially if you import from another import, like me).

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` } } })(); 

Hope this helps someone who is dumb like me.


Original answer:

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

So I ended up doing something similar to @Caranicas answer. I created a utility function that finds the imported file, calls it outside the class in IIFE, and then makes it a class property, for example:

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); } 

SIC / 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> 

SIC / 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 would be nice to call it more than once for each import, and only for an import that actually needs it.

+1
source

For multi-populated HTML imports (npm @ webcomponents / html-import ^ 1.2), the <template> component ends up placing somewhere in the main title of the document. With native HTML import, it ends up being placed in a separate document. A reliable way to find a pattern in both cases:

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

Give each template a unique identifier, such as a component name, to select the correct template in case of filling (in this regard, the manual may be too simple)

0
source

All Articles