I am currently studying CustomHtmlElements and I have a misunderstanding about the order in which the constructors are invoked.
For example, if I have two CustomHtmlElements:
class ExForm extends HTMLElement {
constructor() {
super();
console.log(`${this.getAttribute('name')} constructor ${performance.now()}`);
//debugger;
}
connectedCallback() {
console.log(`${this.getAttribute('name')} connected ${performance.now()}`);
}
}
customElements.define("ex-form", ExForm);
class ExInput extends HTMLElement {
constructor() {
super();
console.log(`${this.getAttribute('name')} constructor ${performance.now()}`);
//debugger;
}
connectedCallback() {
console.log(`${this.getAttribute('name')} connected ${performance.now()}`);
}
}
customElements.define("ex-input", ExInput);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<ex-form name="form1">
<ex-input name="input1" ></ex-input>
</ex-form>
<ex-form name="form2">
<ex-input name="input2"></ex-input>
</ex-form>
</body>
<script src="./index.js"></script>
</html>
I was expecting that the order of constructors execution would be:
form1, input1, form2, input2
However, when I executed the code, the order was:
form1, form2, input1, input2
Can someone clarify why there is a discrepancy between the order of constructor execution and the order in which the HTML elements are rendered to the page?
It depends on when the Web Component is defined.
Your JS code in a StackOverflow Code snippet will run after the DOM is parsed, thus will upgrade the existing Custom Elements (initially parsed as HTMLUnknownElement)
ex-form is defined first, so all existing <ex-form> will be upgraded first.
Then ex-input is defined, and all existing <ex-input> are upgraded next.
(see snippet below) If you move the declaration before the DOM is parsed, you get the order you expect:
In general: don't rely on order in the DOM, you have no control on when the user/developer loads your Web Component file.
There is customElements.whenDefined( ) if you need to check for dependencies.
<script>
class BaseClass extends HTMLElement {
constructor() {
super();
console.log(`constructor ${this.nodeName} `, this.innerHTML);
}
connectedCallback() {
console.log(`${this.nodeName} connected ${this.innerHTML}`);
}
}
customElements.define("ex-form", class extends BaseClass {});
customElements.define("ex-input", class extends BaseClass {});
</script>
<ex-form>
<ex-input></ex-input>
<ex-input></ex-input>
</ex-form>
<ex-form>
<ex-input></ex-input>
</ex-form>
Related
I'm making a web component that will show "Hello {name}!" where {name} comes from name="foo". When I try it I don't get any errors but it just displays "Hello null!".
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="./script.js"></script>
</head>
<body>
<hello-world name="Joe"></hello-world>
</body>
</html>
script.js:
class HelloWorld extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
const p = document.createElement('p');
p.innerHTML = `Hello ${this.getAttribute('name')}!`;
this.shadowRoot.append(p);
}
}
customElements.define('hello-world', HelloWorld);
In any situation lets assume that name will always have an input.
You must not read attributes in the constructor, otherwise you're violating the specification:
The element's attributes and children must not be inspected, as in the non-upgrade case none will be present, and relying on upgrades makes the element less usable.
From: https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance
You must delay this kind of work until the connectedCallback triggers, or, in the case of attributes, configure a proper attributeChangedCallback.
Conforming to the spec will also solve your initial issue, and it will greatly enhance the functionality and usefulness of your web component.
Please note that it's also not the smartest of ideas to pick an attribute name that already exists in the HTML specification, as a universal attribute.
class HelloWorld extends HTMLElement {
p = document.createElement('p');
constructor() {
super();
this.attachShadow({ mode: 'open' }).append(this.p);
}
static get observedAttributes() { return ['greeting-name']; }
attributeChangedCallback(attr, oldVal, newVal) {
if (oldVal === newVal) return; // nothing to do
switch (attr) {
case 'greeting-name':
this.p.textContent = `Hello ${newVal || 'world'}!`;
break;
}
}
connectedCallback() {
if (!this.getAttribute('greeting-name')) { this.setAttribute('greeting-name', 'world'); }
}
}
customElements.define('hello-world', HelloWorld);
<hello-world greeting-name="Joe"></hello-world>
<hello-world id="foo"></hello-world>
<input type="text" oninput="document.getElementById('foo').setAttribute('greeting-name', this.value)" placeholder="Type a name" />
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.
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 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.
I am playing with the new stuff in JavaScript/ES6. I get an Uncaught ReferenceError: this is not defined(...) player.js:5 in my code. As far as I see, there are no errors here! Is this a bug? Any workarounds?
index.html
<html>
<head>
<script type="text/javascript" src="js/entity.js"></script>
<script type="text/javascript" src="js/player.js"></script>
<link href="css/style.css" rel="stylesheet" type="text/css">
<title>Test</title>
</head>
<body>
<canvas id="screen" width=500 height=500></canvas>
<script type="text/javascript">initialize();</script>
</body>
</html>
entity.js
"use strict";
class Entity {
constructor() {
console.log("Entity");
}
}
player.js
"use strict";
class Player extends Entity {
constructor() {
console.log("Created"); // <- error here
}
}
This is a fact of the new class syntax. Your subclass needs to call super() in order for the class to be properly initialized, e.g.
super(arg1, arg2, argN);
with whatever arguments the parent constructor needs.
It is required that, if execution reaches the end of a constructor function, the value of this needs to have been initialized to something. You either need to be in a base class (where this is auto-initialized), have called super() so this is initialized, or returned an alternative object.
class Player extends Entity {
constructor() {
super();
console.log("Created"); ;// error here
}
}
You can think of it like constructor functions kind of have an automatic return this at the end of them.