How to create a custom HTML element with Ace Editor? - javascript

I am trying to create a custom HTML element using Web Components that will contain an instance of Ace Editor. My attempted solution is the following
customElements.define("test-editor", class extends HTMLElement {
constructor() {
super();
const editorContainer = document.createElement("div");
editorContainer.setAttribute("id", "editor_container");
const root = this.attachShadow({mode: "open"});
root.appendChild(editorContainer);
}
connectedCallback() {
ace.edit("editor_container");
}
});
I then try to use this custom element in the following HTML (custom_element.js contains the above)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
html, body, #editor_container {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<test-editor></test-editor>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ace.js"></script>
<script src="custom_element.js"></script>
</body>
</html>
When I load this page in either Chrome or Firefox, I get the following error in the console
Uncaught Error: ace.edit can't find div #editor_container
at Object.t.edit (ace.js:1)
at HTMLElement.connectedCallback (custom_element.js:17)
at custom_element.js:3
Is there any way I can embed an instance of Ace Editor in a custom element?

There is a demo of using shadow-dom in ace repository
The important part is to pass dom element instead of a string to the edit method
And to call editor.renderer.attachToShadowRoot

You have attached a shadowDOM to your Component.
Because the ACE Editor code is loaded in the page DOM (green),
it can only find ACE containers in the page DOM (green).
Note: You also had the <style> in pageDOM. That will never style elements in shadowDOM.
The whole essence of shadowDOM is encapsulation
See: https://developers.google.com/web/fundamentals/web-components/shadowdom
You have 2 options to make the ACE editor work in a Custom Element:
Use shadowDOM, does need a lot of scripting. But you can get multiple editors in one page. See: https://raw.githack.com/ajaxorg/ace/master/demo/shadow-dom.html
not use shadowDOM
(and while we are at it, let the Element load all dependencies):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script id=myEditor>
customElements.define("my-editor", class extends HTMLElement {
connectedCallback() {
const script = document.createElement("SCRIPT");
script.src = "//cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ace.js";
script.onload = () => {
this.innerHTML = `<div id=editor_container>${myEditor.innerHTML}</div>`;
ace.edit("editor_container", {
mode: "ace/mode/javascript",
theme: "ace/theme/cobalt"
});
}
document.head.append(script);
}
});
</script>
<style>
html,
body,
#editor_container {
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<my-editor></my-editor>
</body>
</html>
Or checkout more config examples at:
https://jsfiddle.net/WebComponents/jznf45sg/

If you are looking for the simplest way to add an editor to a web component the Ace team does provide an api, just build an editor and then add it to the shadow dom like this:
aceEditor.renderer.attachToShadowRoot();
Here is one I built to display a snippet of code from a file (it displays the file name as well as the code):
class CodeSnippet extends HTMLElement {
constructor(fileName, fileContents) {
super();
this._fileName = fileName;
this._fileContents = fileContents;
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(this.getTemplate());
}
getTemplate() {
const template = document.createElement('template');
template.innerHTML = `
<style>
.fileName {
color: gray;
}
</style>
<div class="fileName"></div>
<div class="codeSnippet"></div>`;
return template.content.cloneNode(true);
}
connectedCallback() {
//display the file name
const fileName = this.shadowRoot.querySelector('.fileName');
fileName.innerHTML = this._fileName;
//used to set the height of the ace editor (always have at least one line)
let numNewLines = 1;
//collect the newlines (null if there are none)
const allNewLines = this._fileContents.match(/\n/g);
if(allNewLines) {
//add an extra for the last line without a newline on it
numNewLines = allNewLines.length + 1;
}
//get the element where you want to add the editor
const codeSnippet = this.shadowRoot.querySelector('.codeSnippet');
//build an ace editor like normal
const aceEditor = ace.edit(codeSnippet, {
theme: "ace/theme/monokai",
mode: "ace/mode/java",
value: this._fileContents, //code to display
showPrintMargin: false,
readOnly: true,
fontSize: 16,
maxLines: numNewLines, //auto set the height of the editor
highlightActiveLine: false,
highlightGutterLine: false
});
//attach the ace editor to the shadow dom
aceEditor.renderer.attachToShadowRoot();
}
}
window.customElements.define('code-snippet', CodeSnippet);
Don't forget that you need access to the Ace library somewhere in your code. I use a CDN:
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ace.js" type="text/javascript" charset="utf-8"></script>
To test it you can add them to another component:
const codeView = this.shadowRoot.querySelector('.codeView');
const snippet1 = new CodeSnippet('/testFile1.txt', 'line 1\nline 2\nline 3');
codeView.appendChild(snippet1);
const snippet2 = new CodeSnippet('/testFile2.txt', 'line 1\nline 2\nline 3\n');
codeView.appendChild(snippet2);
const snippet3 = new CodeSnippet('/testFile3.txt', '');
codeView.appendChild(snippet3);

Related

Custom element not picking up attributes

I am trying to have a look at custom elements and how they work and while the examples on MDN work fine I'm seemingly unable to replicate them myself.
The MDN article is here.
This is a working example from MDN.
My problem is that I can't ever seem to pass attributes into my component, they always come out as null instead of passing over the value of the parameter.
My JS is (test.js)
class PopUpInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super();
// Create a shadow root
const shadow = this.attachShadow({mode: 'open'});
// Create spans
const wrapper = document.createElement('span');
const info = document.createElement('span');
// Take attribute content and put it inside the info span
const text = this.getAttribute('foo'); // <-- this always returns null
info.textContent = `(${text})`;
shadow.appendChild(wrapper);
wrapper.appendChild(info);
}
}
// Define the new element
customElements.define('popup-info', PopUpInfo);
And my Html:
<html>
<head>
<script src="test.js"></script>
</head>
<body>
<hr>
<popup-info foo="Hello World"></popup-info>
<hr>
</body>
</html>
What I'm expecting to see on screen is the text
(Hello World)
but all I ever see is
(null)
When I debug I can see that this.attributes has a length of 0 so it's not being passed in.
Has anyone seen this before when creating custom elements?
Keep Emiel his answer as the correct one.
Just to show there are alternative and shorter notations possible:
customElements.define('popup-info', class extends HTMLElement {
static get observedAttributes() {
return ['foo'];
}
constructor() {
const wrapper = document.createElement('span');
super().attachShadow({mode:'open'})// both SETS and RETURNS this.shadowRoot
.append(wrapper);
this.wrapper = wrapper;
}
attributeChangedCallback(name, oldValue, newValue) {
switch(name) {
case 'foo':
this.wrapper.textContent = `(${newValue})`;
break;
}
}
});
<popup-info
foo="Hello World"
onclick="this.setAttribute('foo','Another world')"
>
</popup-info>
Although your example seems to run fine when I try to run it here in a snippet, I still want to make a suggestion to improve it.
Use the observedAttributes static getter to define a list of attributes which the component should keep an eye on. When the value of an attribute has been changed and the name of the attribute is in the list, then attributeChangedCallback callback is called. In there you can assert logic on what to do whenever you attribute value has been changed.
In this case you could build your string that you desire. This also has the side effect that whenever the attribute value is changed again, the string will be updated.
class PopUpInfo extends HTMLElement {
/**
* Observe the foo attribute for changes.
*/
static get observedAttributes() {
return ['foo'];
}
constructor() {
super();
const shadow = this.attachShadow({
mode: 'open'
});
const wrapper = document.createElement('span');
const info = document.createElement('span');
wrapper.classList.add('wrapper');
wrapper.appendChild(info);
shadow.appendChild(wrapper);
}
/**
* Returns the wrapper element from the shadowRoot.
*/
get wrapper() {
return this.shadowRoot.querySelector('.wrapper')
}
/**
* Is called when observed attributes have a changed value.
*/
attributeChangedCallback(attrName, oldValue, newValue) {
switch(attrName) {
case 'foo':
this.wrapper.textContent = `(${newValue})`;
break;
}
}
}
// Define the new element
customElements.define('popup-info', PopUpInfo);
<html>
<head>
<script src="test.js"></script>
</head>
<body>
<hr>
<popup-info foo="Hello World"></popup-info>
<hr>
</body>
</html>
You're missing a defer attribute in your script import within the HTML and it is not loading properly, thats the problem. The defer attribute allows the script to be executed after the page is parsed
class PopUpInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super()
// Create a shadow root
const shadow = this.attachShadow({ mode: 'open' })
// Create spans
const wrapper = document.createElement('span')
const info = document.createElement('span')
// Take attribute content and put it inside the info span
const text = this.getAttribute('foo') // <-- this always returns null
info.textContent = `(${text})`
shadow.appendChild(wrapper)
wrapper.appendChild(info)
}
}
// Define the new element
customElements.define('popup-info', PopUpInfo)
<html>
<head>
<script src="app.js" defer></script>
</head>
<body>
<hr />
<popup-info foo="Hello World"></popup-info>
<hr />
</body>
</html>

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

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

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

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