web component (vanilla, no polymer): how to load <template> content? - javascript

i'm new on web component. I checked some example, but i really can't figure out how to load (insert in the DOM) the content of a of a separate web component. Starting from this example , I put this code in a file named my-element.html:
<template id="my-element">
<p>Yes, it works!</p>
</template>
<script>
document.registerElement('my-element', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({mode: 'open'});
const t = document.querySelector('#my-element');
const instance = t.content.cloneNode(true);
shadowRoot.appendChild(instance);
}
});
</script>
This is my index.html:
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>my titile</title>
<link rel="import" href="my-element.html">
</head>
<body>
Does it work?
<my-element></my-element>
</body>
</html>
I'm on latest Chrome 56, so i don't need polyfill. I run polyserve, and only "Does it works?" appears. I tried (like the original example) the "customElements.define" syntax instead of "document.registerElement", but won't work. Have you some ideas? And what have I to change if i don't want to use shadow dom?
thanks

It's because when you do:
document.querySelector( '#my-element' );
...document refers to the main document index.html
If you want to get the template, you should use instead document.currentScript.ownerDocument
var importedDoc = document.currentScript.ownerDocument;
customElements.define('my-element', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({mode: 'open'});
const t = importedDoc.querySelector('#my-element');
const instance = t.content.cloneNode(true);
shadowRoot.appendChild(instance);
}
});
Note that document.currentScript is a global variable, so it refers to your imported document only when it is currently parsed. That's why it's value is saved in a variable (here: importedDoc) to be reusable later (in the constrcutor call)
If you have multiple imported document you may want to isolate it in a closure (as explained in this post):
( function ( importedDoc )
{
//register element
} )(document.currentScript.ownerDocument);

Related

Which is the recommended way to import WebComponents? [duplicate]

I have a web component x-counter, which is in a single file.
const template = document.createElement('template');
template.innerHTML = `
<style>
button, p {
display: inline-block;
}
</style>
<button aria-label="decrement">-</button>
<p>0</p>
<button aria-label="increment">+</button>
`;
class XCounter extends HTMLElement {
set value(value) {
this._value = value;
this.valueElement.innerText = this._value;
}
get value() {
return this._value;
}
constructor() {
super();
this._value = 0;
this.root = this.attachShadow({ mode: 'open' });
this.root.appendChild(template.content.cloneNode(true));
this.valueElement = this.root.querySelector('p');
this.incrementButton = this.root.querySelectorAll('button')[1];
this.decrementButton = this.root.querySelectorAll('button')[0];
this.incrementButton
.addEventListener('click', (e) => this.value++);
this.decrementButton
.addEventListener('click', (e) => this.value--);
}
}
customElements.define('x-counter', XCounter);
Here the template is defined as using JavaScript and html contents are added as inline string. Is there a way to separate template to an x-counter.html file, css to say, x-counter.css and corresponding JavaScript code to xcounter.js and load them in index.html?
Every example I lookup has web components mixed. I would like to have separation of concerns, but I am not sure how to do that with components. Could you provide a sample code? Thanks.
In the main file, use <script> to load the Javascript file x-counter.js.
In the Javascript file, use fetch() to load the HTML code x-counter.html.
In the HTML file, use <link rel="stylesheet"> to load the CSS file x-counter.css.
CSS file : x-counter.css
button, p {
display: inline-block;
color: dodgerblue;
}
HTML file : x-counter.html
<link rel="stylesheet" href="x-counter.css">
<button aria-label="decrement">-</button>
<p>0</p>
<button aria-label="increment">+</button>
Javascript file : x-counter.js
fetch("x-counter.html")
.then(stream => stream.text())
.then(text => define(text));
function define(html) {
class XCounter extends HTMLElement {
set value(value) {
this._value = value;
this.valueElement.innerText = this._value;
}
get value() {
return this._value;
}
constructor() {
super();
this._value = 0;
var shadow = this.attachShadow({mode: 'open'});
shadow.innerHTML = html;
this.valueElement = shadow.querySelector('p');
var incrementButton = shadow.querySelectorAll('button')[1];
var decrementButton = shadow.querySelectorAll('button')[0];
incrementButton.onclick = () => this.value++;
decrementButton.onclick = () => this.value--;
}
}
customElements.define('x-counter', XCounter);
}
Main file : index.html
<html>
<head>
<!-- ... -->
<script src="x-counter.js"></script>
</head>
<body>
<x-counter></x-counter>
</body>
</html>
A generic pattern using top level await without side-effects:
my-component/
element.js
template.html
styles.css
template.html (be sure to link to styles.css)
<template>
<link rel="stylesheet" href="./styles.css" />
<!-- other HTML/Slots/Etc. -->
<slot></slot>
</template>
styles.css (regular CSS file)
:host {
border: 1px solid red;
}
element.js (uses top level await in export)
const setup = async () => {
const parser = new DOMParser()
const resp = await fetch('./template.html')
const html = await resp.text()
const template = parser.parseFromString(html, 'text/html').querySelector('template')
return class MyComponent extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open'}).appendChild(template.content.cloneNode(true))
}
// Rest of element implementation...
}
}
export default await setup()
index.html (loading and defining the element)
<!doctype html>
<html>
<head>
<title>Custom Element Separate Files</title>
<script type="module">
import MyComponent from './element.js'
if (!customElements.get('my-component')) {
customElements.define('my-component', MyComponent)
}
</script>
</head>
<body>
<my-component>hello world</my-component>
</body>
</html>
You can and should make side-effects (like registering a custom element in the global scope) explicit. Aside from creating some init function to call on your element, you can also provide a distinct import path, for example:
defined.js (sibling to element.js)
import MyComponent from './element.js'
const define = async () => {
let ctor = null
customElements.define('my-component', MyComponent)
ctor = await customElements.whenDefined('my-component')
return ctor
}
export default await define()
index.html (side-effect made explicit via import path)
<!doctype html>
<html>
<head>
<title>Custom Element Separate Files</title>
<script type="module" src="./defined.js"></script>
</head>
<body>
<my-component>hello world</my-component>
</body>
</html>
This approach can also support arbitrary names when defining the custom element by including something like this inside define:
new URL(import.meta.url).searchParams.get('name')
and then passing the name query param in the import specifier:
<script type="module" src="./defined.js?name=custom-name"></script>
<custom-name>hello</custom-name>
Here's an example snippet using tts-element that combines all three approaches (see the network tab in dev console):
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>tts-element combined example</title>
<style>
text-to-speech:not(:defined), my-tts:not(:defined), speech-synth:not(:defined) {
display: none;
}
</style>
<script type="module" src="https://unpkg.com/tts-element/dist/text-to-speech/defined.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/tts-element#0.0.3-beta/dist/text-to-speech/defined.js?name=my-tts"></script>
<script type="module">
import ctor from 'https://unpkg.com/tts-element/dist/text-to-speech/element.js'
customElements.define('speech-synth', ctor)
</script>
</head>
<body>
<text-to-speech>Your run-of-the-mill text-to-speech example.</text-to-speech>
<my-tts>Example using the "name" query parameter.</my-tts>
<speech-synth>Example using element.js.</speech-synth>
</body>
</html>
I've wanted a solution for this as well - but didn't find a satisfying way to do this.
I see that fetching the CSS/HTML has been suggested here - but I find this a bit troublesome. For me this seems to be a bit too much overhead and might cause some problems or performant issues.
I wanted to see if I could find other solutions.
"CSS Module scripts" seems to bee coming soon, but browsers like Safari doesn't support this.
Webpack compiling with raw-loader is another solution - or Rollup. But I find that it is too slow or too much config.
I ended up creating my own CLI tool - which I have set up in my IDE (PHPStorm) - so it automatically compiles CSS and HTML into native JavaScript modules that exports ready HTMLTemplate with code.
I have also an example on how to achieve the same in VSCode.
Maybe this could be an alternative approach for some - so I wanted to share it.
It is available as a NPM package here:
https://www.npmjs.com/package/csshtml-module
I can now write HTML files and even SCSS files - which PHPStorm automatically compiles to CSS with Autoprefixer and CSSO (optimizer) - and then it compiles these to a native JS module with template.
Like this:
export const template = document.createElement('template');
template.innerHTML = `<style>button{background-color:red}</style><button>Hello</button>`;
You can set it to compile a single file as well, like CSS - which compiles to a module:
// language=css
export const css = `button {
background-color: red;
}`;

How to separate web components to individual files and load them?

I have a web component x-counter, which is in a single file.
const template = document.createElement('template');
template.innerHTML = `
<style>
button, p {
display: inline-block;
}
</style>
<button aria-label="decrement">-</button>
<p>0</p>
<button aria-label="increment">+</button>
`;
class XCounter extends HTMLElement {
set value(value) {
this._value = value;
this.valueElement.innerText = this._value;
}
get value() {
return this._value;
}
constructor() {
super();
this._value = 0;
this.root = this.attachShadow({ mode: 'open' });
this.root.appendChild(template.content.cloneNode(true));
this.valueElement = this.root.querySelector('p');
this.incrementButton = this.root.querySelectorAll('button')[1];
this.decrementButton = this.root.querySelectorAll('button')[0];
this.incrementButton
.addEventListener('click', (e) => this.value++);
this.decrementButton
.addEventListener('click', (e) => this.value--);
}
}
customElements.define('x-counter', XCounter);
Here the template is defined as using JavaScript and html contents are added as inline string. Is there a way to separate template to an x-counter.html file, css to say, x-counter.css and corresponding JavaScript code to xcounter.js and load them in index.html?
Every example I lookup has web components mixed. I would like to have separation of concerns, but I am not sure how to do that with components. Could you provide a sample code? Thanks.
In the main file, use <script> to load the Javascript file x-counter.js.
In the Javascript file, use fetch() to load the HTML code x-counter.html.
In the HTML file, use <link rel="stylesheet"> to load the CSS file x-counter.css.
CSS file : x-counter.css
button, p {
display: inline-block;
color: dodgerblue;
}
HTML file : x-counter.html
<link rel="stylesheet" href="x-counter.css">
<button aria-label="decrement">-</button>
<p>0</p>
<button aria-label="increment">+</button>
Javascript file : x-counter.js
fetch("x-counter.html")
.then(stream => stream.text())
.then(text => define(text));
function define(html) {
class XCounter extends HTMLElement {
set value(value) {
this._value = value;
this.valueElement.innerText = this._value;
}
get value() {
return this._value;
}
constructor() {
super();
this._value = 0;
var shadow = this.attachShadow({mode: 'open'});
shadow.innerHTML = html;
this.valueElement = shadow.querySelector('p');
var incrementButton = shadow.querySelectorAll('button')[1];
var decrementButton = shadow.querySelectorAll('button')[0];
incrementButton.onclick = () => this.value++;
decrementButton.onclick = () => this.value--;
}
}
customElements.define('x-counter', XCounter);
}
Main file : index.html
<html>
<head>
<!-- ... -->
<script src="x-counter.js"></script>
</head>
<body>
<x-counter></x-counter>
</body>
</html>
A generic pattern using top level await without side-effects:
my-component/
element.js
template.html
styles.css
template.html (be sure to link to styles.css)
<template>
<link rel="stylesheet" href="./styles.css" />
<!-- other HTML/Slots/Etc. -->
<slot></slot>
</template>
styles.css (regular CSS file)
:host {
border: 1px solid red;
}
element.js (uses top level await in export)
const setup = async () => {
const parser = new DOMParser()
const resp = await fetch('./template.html')
const html = await resp.text()
const template = parser.parseFromString(html, 'text/html').querySelector('template')
return class MyComponent extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open'}).appendChild(template.content.cloneNode(true))
}
// Rest of element implementation...
}
}
export default await setup()
index.html (loading and defining the element)
<!doctype html>
<html>
<head>
<title>Custom Element Separate Files</title>
<script type="module">
import MyComponent from './element.js'
if (!customElements.get('my-component')) {
customElements.define('my-component', MyComponent)
}
</script>
</head>
<body>
<my-component>hello world</my-component>
</body>
</html>
You can and should make side-effects (like registering a custom element in the global scope) explicit. Aside from creating some init function to call on your element, you can also provide a distinct import path, for example:
defined.js (sibling to element.js)
import MyComponent from './element.js'
const define = async () => {
let ctor = null
customElements.define('my-component', MyComponent)
ctor = await customElements.whenDefined('my-component')
return ctor
}
export default await define()
index.html (side-effect made explicit via import path)
<!doctype html>
<html>
<head>
<title>Custom Element Separate Files</title>
<script type="module" src="./defined.js"></script>
</head>
<body>
<my-component>hello world</my-component>
</body>
</html>
This approach can also support arbitrary names when defining the custom element by including something like this inside define:
new URL(import.meta.url).searchParams.get('name')
and then passing the name query param in the import specifier:
<script type="module" src="./defined.js?name=custom-name"></script>
<custom-name>hello</custom-name>
Here's an example snippet using tts-element that combines all three approaches (see the network tab in dev console):
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8" />
<title>tts-element combined example</title>
<style>
text-to-speech:not(:defined), my-tts:not(:defined), speech-synth:not(:defined) {
display: none;
}
</style>
<script type="module" src="https://unpkg.com/tts-element/dist/text-to-speech/defined.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/tts-element#0.0.3-beta/dist/text-to-speech/defined.js?name=my-tts"></script>
<script type="module">
import ctor from 'https://unpkg.com/tts-element/dist/text-to-speech/element.js'
customElements.define('speech-synth', ctor)
</script>
</head>
<body>
<text-to-speech>Your run-of-the-mill text-to-speech example.</text-to-speech>
<my-tts>Example using the "name" query parameter.</my-tts>
<speech-synth>Example using element.js.</speech-synth>
</body>
</html>
I've wanted a solution for this as well - but didn't find a satisfying way to do this.
I see that fetching the CSS/HTML has been suggested here - but I find this a bit troublesome. For me this seems to be a bit too much overhead and might cause some problems or performant issues.
I wanted to see if I could find other solutions.
"CSS Module scripts" seems to bee coming soon, but browsers like Safari doesn't support this.
Webpack compiling with raw-loader is another solution - or Rollup. But I find that it is too slow or too much config.
I ended up creating my own CLI tool - which I have set up in my IDE (PHPStorm) - so it automatically compiles CSS and HTML into native JavaScript modules that exports ready HTMLTemplate with code.
I have also an example on how to achieve the same in VSCode.
Maybe this could be an alternative approach for some - so I wanted to share it.
It is available as a NPM package here:
https://www.npmjs.com/package/csshtml-module
I can now write HTML files and even SCSS files - which PHPStorm automatically compiles to CSS with Autoprefixer and CSSO (optimizer) - and then it compiles these to a native JS module with template.
Like this:
export const template = document.createElement('template');
template.innerHTML = `<style>button{background-color:red}</style><button>Hello</button>`;
You can set it to compile a single file as well, like CSS - which compiles to a module:
// language=css
export const css = `button {
background-color: red;
}`;

HTML Import: How to obtain correct document object within imported html? [duplicate]

I made a simple example using Web Components with two custom elements (v1) where one is nested in another.
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Example</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="import" href="app-container.html">
</head>
<body>
<app-container></app-container>
</body>
</html>
app-container.html:
<link rel="import" href="toolbar.html">
<template id="app-container">
<app-toolbar></app-toolbar>
</template>
<script>
customElements.define('app-container', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: 'open' });
const content = document.currentScript.ownerDocument.querySelector('#app-container').content;
shadowRoot.appendChild(content.cloneNode(true));
}
});
</script>
toolbar.html:
<template id="app-toolbar">
<p>Ok!</p>
</template>
<script>
customElements.define('app-toolbar', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: 'open' });
const content = document.currentScript.ownerDocument.querySelector('#app-toolbar').content;
shadowRoot.appendChild(content.cloneNode(true));
}
});
</script>
But in the toolbar.html document.currentScript is the same as in the app-container.html and hence querySelector('#app-toolbar') can't find template with id app-toolbar. How to solve this problem?
Example tested on Chrome 55 (without polyfill).
document.currentScript contains a reference to the script that is currently parsed and executed. Therefore it is not valid anymore for your purpose when the constructor() function is called (from another script).
Instead you shoud save its value in a variable at the beginning of the script, and use this variable in the constructor:
<script>
var currentScript = document.currentScript
customElements.define( ... )
...
</script>
If you have multiple scripts, you should use distinct names.
Alternately, you can encapsulate the ephemeral value in a closure:
(function(owner) {
customElements.define('app-container', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: 'open' });
const content = owner.querySelector('#app-container').content;
shadowRoot.appendChild(content.cloneNode(true));
}
});
})(document.currentScript.ownerDocument);
Here the value document.currentScript.ownerDocument is assigned to the owner argument which is still defined correctly when constructor() is called.
owner is locally defined so you can use the same name in the other document.

Nested element (web component) can't get its template

I made a simple example using Web Components with two custom elements (v1) where one is nested in another.
index.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Example</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="import" href="app-container.html">
</head>
<body>
<app-container></app-container>
</body>
</html>
app-container.html:
<link rel="import" href="toolbar.html">
<template id="app-container">
<app-toolbar></app-toolbar>
</template>
<script>
customElements.define('app-container', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: 'open' });
const content = document.currentScript.ownerDocument.querySelector('#app-container').content;
shadowRoot.appendChild(content.cloneNode(true));
}
});
</script>
toolbar.html:
<template id="app-toolbar">
<p>Ok!</p>
</template>
<script>
customElements.define('app-toolbar', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: 'open' });
const content = document.currentScript.ownerDocument.querySelector('#app-toolbar').content;
shadowRoot.appendChild(content.cloneNode(true));
}
});
</script>
But in the toolbar.html document.currentScript is the same as in the app-container.html and hence querySelector('#app-toolbar') can't find template with id app-toolbar. How to solve this problem?
Example tested on Chrome 55 (without polyfill).
document.currentScript contains a reference to the script that is currently parsed and executed. Therefore it is not valid anymore for your purpose when the constructor() function is called (from another script).
Instead you shoud save its value in a variable at the beginning of the script, and use this variable in the constructor:
<script>
var currentScript = document.currentScript
customElements.define( ... )
...
</script>
If you have multiple scripts, you should use distinct names.
Alternately, you can encapsulate the ephemeral value in a closure:
(function(owner) {
customElements.define('app-container', class extends HTMLElement {
constructor() {
super();
let shadowRoot = this.attachShadow({ mode: 'open' });
const content = owner.querySelector('#app-container').content;
shadowRoot.appendChild(content.cloneNode(true));
}
});
})(document.currentScript.ownerDocument);
Here the value document.currentScript.ownerDocument is assigned to the owner argument which is still defined correctly when constructor() is called.
owner is locally defined so you can use the same name in the other document.

A web component with ECMA6

index.html
<!DOCTYPE html>
<html>
<head>
<script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script>
<script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script>
<script>
traceur.options.experimental = true;
</script>
<link rel="import" href="x-item.html">
</head>
<body>
<x-item></x-item>
</body>
</html>
and my web component:
x-item.html
<template id="itemtemplate">
<span>test</span>
</template>
<script type="module">
class Item extends HTMLElement {
constructor() {
let owner = document.currentScript.ownerDocument;
let template = owner.querySelector("#itemtemplate");
let clone = template.content.cloneNode(true);
let root = this.createShadowRoot();
root.appendChild(clone);
}
}
Item.prototype.createdCallback = Item.prototype.constructor;
Item = document.registerElement('x-item', Item);
</script>
and I get no error nor what I expect to be displayed, any idea if this should actually work?
Is this how one would extend an HTMLElement in ECMA6 syntax?
E: putting it altogether in one page solves the problem at least now I know its the right way to create a custom component, but the problem is having it in a separate file I think it has to do with how traceur handles <link rel="import" href="x-item.html"> I tried adding the type attribute to the import with no luck.
Traceur's inline processor does not appear to have support for finding <script> tags inside <link import>. All of traceur's code seems to access document directly, which results in traceur only looking at index.html and never seeing any <scripts> inside x-item.html. Here's a work around that works on Chrome. Change x-item.html to be:
<template id="itemtemplate">
<span>test</span>
</template>
<script type="module">
(function() {
let owner = document.currentScript.ownerDocument;
class Item extends HTMLElement {
constructor() {
// At the point where the constructor is executed, the code
// is not inside a <script> tag, which results in currentScript
// being undefined. Define owner above at compile time.
//let owner = document.currentScript.ownerDocument;
let template = owner.querySelector("#itemtemplate");
let clone = template.content.cloneNode(true);
let root = this.createShadowRoot();
root.appendChild(clone);
}
}
Item.prototype.createdCallback = Item.prototype.constructor;
Item = document.registerElement('x-item', Item);
})();
</script>
<script>
// Boilerplate to get traceur to compile the ECMA6 scripts in this include.
// May be a better way to do this. Code based on:
// new traceur.WebPageTranscoder().selectAndProcessScripts
// We can't use that method as it accesses 'document' which gives the parent
// document, not this include document.
(function processInclude() {
var doc = document.currentScript.ownerDocument,
transcoder = new traceur.WebPageTranscoder(doc.URL),
selector = 'script[type="module"],script[type="text/traceur"]',
scripts = doc.querySelectorAll(selector);
if (scripts.length) {
transcoder.addFilesFromScriptElements(scripts, function() {
console.log("done processing");
});
}
})();
</script>
Another possible solution would be to pre-compile the ECMA6 into ECMA5 and include the ECMA5 only. This would avoid the problem of traceur not finding the <script> tags in the import and would remove the need for the boilerplate.

Categories

Resources