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.
Related
I have a web-component at root level. The simplified version of which is shown below:
class AppLayout {
constructor() {
super();
this.noShadow = true;
}
connectedCallback() {
super.connectedCallback();
this.render();
this.insertAdjacentHTML("afterbegin", this.navigation);
}
render() {
this.innerHTML = this.template;
}
get template() {
return `
<h1>Hello</h1>
`;
}
navigation = `
<script type="module">
import './components/nav-bar.js'
</script>
`;
}
customElements.define('app-layout', AppLayout);
I want to load a script after this component loads. The script creates html for navigation and tries to add it to the app-layout element shown above. However, even though, it does find the app-layout element, it is unable to append the navBar element. It is, however, able to append the navBar to the body of the html. Any ideas what I'm missing.
const navLinks =
`<ul>
<li>Some</li>
<li>Links</li>
</ul>
`;
const navBar = document.createElement('nav');
navBar.innerHTML = navLinks;
const appLayout = document.querySelector('app-layout'); // works with 'body' but not with 'appLayout'
console.log(appLayout); // Logs correct element
appLayout.appendChild(navBar);
I know that what I'm trying to do here (loading a script inside a web component) is not ideal, however, I would like to still understand why the above doesn't work.
using innerHTML or in your case insertAdjacentHTML to add <script> tags to the document doesn't work because browsers historically try to prevent potential cross site script attacks (https://www.w3.org/TR/2008/WD-html5-20080610/dom.html#innerhtml0)
What you could do is something like:
const s = document.createElement("script");
s.type = "module";
s.innerText = `import './components/nav-bar.js'`;
this.append(s);
// or simply directly without the script: `import('./comp..')` if it is really your only js in the script tag.
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;
}`;
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;
}`;
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);
I'm creating an HTML element using Polymer, and I want it to be able to work with an ES6 class I've written. Therefore, I need to import the class first and then register the element, which is what I do:
(function() {
System.import('/js/FoobarModel.js').then(function(m) {
window.FoobarModel = m.default;
window.FoobarItem = Polymer({
is: 'foobar-item',
properties: {
model: Object // instanceof FoobarModel === true
},
// ... methods using model and FoobarModel
});
});
})();
And it works well. But now I want to write a test HTML page to display my component with some dummy data:
<!DOCTYPE html>
<html lang="en">
<head>
<script src="/bower_components/webcomponentsjs/webcomponents.js"></script>
<script src="/bower_components/system.js/dist/system.js"></script>
<script>
System.config({
map:{
traceur: '/bower_components/traceur/traceur.min.js'
}
});
</script>
<link rel="import" href="/html/foobar-item.html">
</head>
<body>
<script>
(function() {
var data = window.data = [
{
city: {
name: 'Foobar City'
},
date: new Date('2012-02-25')
}
];
var view;
for (var i = 0; i < data.length; i++) {
view = new FoobarItem();
view.model = data[i];
document.body.appendChild(view);
}
})();
</script>
</body>
</html>
Which isn't working for one simple reason: the code in the <script> tag is executed before Polymer registers the element.
Thus I'd like to know if there's a way to load the ES6 module synchronously using System.js or even better, if it's possible to listen to a JavaScript event for the element registration (something like PolymerElementsRegistered)?
I've tried the following without success:
window.addEventListener('HTMLImportsLoaded', ...)
window.addEventListener('WebComponentsReady', ...)
HTMLImports.whenReady(...)
In the app/scripts/app.js script from the polymer starter kit, they use auto-binding template and dom-change event
// Grab a reference to our auto-binding template
var app = document.querySelector('#app');
// Listen for template bound event to know when bindings
// have resolved and content has been stamped to the page
app.addEventListener('dom-change', function() {
console.log('Our app is ready to rock!');
});
Also check this thread gives alternatives to the polymer-ready event.