Order of Custom Elements Execution - javascript

I have created webcomponents using customElements.
<x-select>
<x-option>India</x-option>
<x-option>Africa</x-option>
</x-select>
Order of execution would be, at first constructor of <x-select> is called and then the constructor of <x-option> is called. But, I want <z-option> 's constructor to be called first before <z-select>. How could I achieve that?

There are several factors for the order of operations with web components.
When a component with inner elements is created then the outer constructor will be called first. Then the inner constructors are called. This is normal.
If the component is NOT in a real DOM tree then nothing else will happen. No other code is called.
Once the component is placed into the DOM then the connectedCallback is called. Starting from the outside component and then the inside components.
Here is an example not using shadow DOM:
// Class for `<x-select>`
class XSelect extends HTMLElement {
constructor() {
super();
console.log('x-select constructor');
}
connectedCallback() {
console.log('x-select connectedCallback');
}
}
// Define our web component
customElements.define('x-select', XSelect);
// Class for `<x-option>`
class XOption extends HTMLElement {
constructor() {
super();
console.log('x-option constructor');
}
connectedCallback() {
console.log('x-option connectedCallback');
}
}
// Define our web component
customElements.define('x-option', XOption);
<x-select>
<x-option>India</x-option>
<x-option>Africa</x-option>
</x-select>
The console output is:
x-select constructor
x-select connectedCallback
x-option constructor
x-option connectedCallback
x-option constructor
x-option connectedCallback
Even when using shadow DOM in <x-select> the order of operations is the same:
// Class for `<x-select>`
class XSelect extends HTMLElement {
constructor() {
super();
var sd = this.attachShadow({mode:'open'});
sd.innerHTML = '<slot></slot>';
console.log('x-select constructor');
}
connectedCallback() {
console.log('x-select connectedCallback');
}
}
// Define our web component
customElements.define('x-select', XSelect);
// Class for `<x-option>`
class XOption extends HTMLElement {
constructor() {
super();
console.log('x-option constructor');
}
connectedCallback() {
console.log('x-option connectedCallback');
}
}
// Define our web component
customElements.define('x-option', XOption);
<x-select>
<x-option>India</x-option>
<x-option>Africa</x-option>
</x-select>
If we create the components in JavaScript then we can control the order of construction. In my example below I am creating them in the same order, but you can mix that up if you want.
// Class for `<x-select>`
class XSelect extends HTMLElement {
constructor() {
super();
//var sd = this.attachShadow({mode:'open'});
//sd.innerHTML = '<slot></slot>';
console.log('x-select constructor');
}
connectedCallback() {
console.log('x-select connectedCallback');
}
}
// Define our web component
customElements.define('x-select', XSelect);
// Class for `<x-option>`
class XOption extends HTMLElement {
constructor() {
super();
console.log('x-option constructor');
}
connectedCallback() {
console.log(`x-option[${this.textContent}] connectedCallback`);
}
}
// Define our web component
customElements.define('x-option', XOption);
console.log('creating x-select');
var xs = document.createElement('x-select');
console.log('creating x-option 1');
var xo1 = document.createElement('x-option');
console.log('setting text of x-option 1');
xo1.textContent = 'India';
console.log('creating x-option 2');
var xo2 = document.createElement('x-option');
console.log('setting text of x-option 2');
xo2.textContent = 'Africa';
console.log('Adding x-option 1 to x-select');
xs.appendChild(xo1);
console.log('Adding x-option 2 to x-select');
xs.appendChild(xo2);
console.log('Adding x-select to container');
var c = document.getElementById('container');
c.appendChild(xs)
<div id="container"></div>
The constructors are called when I call document.createElement but the connected callback will not trigger until the elements are placed into the DOM.
The console output of the above code is:
creating x-select
x-select constructor
creating x-option 1
x-option constructor
setting text of x-option 1
creating x-option 2
x-option constructor
setting text of x-option 2
Adding x-option 1 to x-select
Adding x-option 2 to x-select
Adding x-select to container
x-select connectedCallback
x-option[India] connectedCallback
x-option[Africa] connectedCallback
One final example is to create a DIV, then set its innerHTML to this:
<x-select>
<x-option>India</x-option>
<x-option>Africa</x-option>
</x-select>
// Class for `<x-select>`
class XSelect extends HTMLElement {
constructor() {
super();
//var sd = this.attachShadow({mode:'open'});
//sd.innerHTML = '<slot></slot>';
console.log('x-select constructor');
}
connectedCallback() {
console.log('x-select connectedCallback');
}
}
// Define our web component
customElements.define('x-select', XSelect);
// Class for `<x-option>`
class XOption extends HTMLElement {
constructor() {
super();
console.log('x-option constructor');
}
connectedCallback() {
console.log(`x-option[${this.textContent}] connectedCallback`);
}
}
// Define our web component
customElements.define('x-option', XOption);
console.log('creating div');
var d = document.createElement('div');
console.log('setting innerHTML of div');
d.innerHTML = `<x-select>
<x-option>India</x-option>
<x-option>Africa</x-option>
</x-select>`;
console.log('Adding div to container');
var c = document.getElementById('container');
c.appendChild(d)
<div id="container"></div>
This now only calls the constructors of the outer element and then the inner elements. Only after the <div> is placed into the DOM are the calls to connectedCallback called. And, again, these are called for the outside element first, then the inside. elements.

There are different ways of achieving that.
Via Javascript
1° Create the <x-option>s:
xo1 = document.createElement( 'x-option' )
2° Create the <x-select>
xs = document.createElement( 'x-select' )
3° Append the <x-option>
xs.appendChild( xo1 )
Via HTML
Defer the init of the parent element in another custom element method, not in its constructor(). Then call this method afert the child elements are created.
<x-select id="xs">
<x-option>India</x-option>
<x-option>Africa</x-option>
<script>xs.init()</script>
</x-select>
<script>
class XSelect extends HTMLElement {
constructor() {
super()
console.log('x-select created')
}
init() {
console.info('x-select init')
}
}
customElements.define('x-select', XSelect)
class XOption extends HTMLElement {
constructor() {
super()
console.log('x-option created')
}
}
customElements.define('x-option', XOption)
</script>
<x-select id="xs">
<x-option id="1">India</x-option>
<x-option id="2">Africa</x-option>
<script>xs.init()</script>
</x-select>

Related

Embedding a custom HTMLElement into another custom HTMLElement

I'm wondering what best practices are for creating an object that extends HTMLElement and embedding it within another object that extends HTMLElement. I'm interested in an implementation that creates these objects and adds them to the DOM programmatically.
Use case: I can create e.g. a Menu component and re-use it throughout my app.
The below code works but I think it can be optimized.
Edit: It was pointed out (and I agree) that there isn’t really a question here.
Here’s a question: I only want to call customElements.define() once per element. What is the scope of this call? I tried to call it in the TodoMenu constructor and then use that element within Todo to no avail.
class Todo extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
const wrapper = document.createElement('div');
customElements.define("todo-menu", TodoMenu);
const todoMenu = document.createElement("todo-menu");
wrapper.appendChild(todoMenu);
shadow.appendChild(wrapper);
}
}
class TodoMenu extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
const wrapper = document.createElement('div');
// Some UI stuff
shadow.appendChild(wrapper);
}
}
customElements.define("todo", Todo);
const todoItem = document.createElement(Todo);
document.getElementById('test-todo2').appendChild(todoItem);

Parent class constructor can't access child fields without Timeout

I am making an application using JavaScript. One thing that I noticed is that when I call super() from child class constructor, I cannot use child class fields.
For example:
class Parent {
constructor() {
console.log(this.x);
}
}
class Child extends Parent {
x = 5;
constructor() {
super();
}
}
Output: undefined
As you see, the parent constructor cannot access the x field of the child constructor. There are similar questions on stackoverflow like:
Access JavaScript class property in parent class
The solution they give is to use an init() method on the Parent class. I realized that every method other than the constructor of the Parent class has access to child fields.
class Parent {
init() {
console.log(this.x);
}
}
class Child extends Parent {
x = 5;
constructor() {
super();
this.init();
}
}
Output: 5
However, I can also get access to the x field by using Timeout with delay of 0:
class Parent {
constructor() {
setTimout(() => console.log(this.x), 0);
}
}
class Child extends Parent {
x = 5;
constructor() {
super();
}
}
Output: 5
All the answers on stackoverflow give the solution, but nobody explains how and why this happens. So my question is:
1. Why the constructor doesn't have access to fields of the child class?
2. Why adding a timeout function gives access to the fields of the child class?

How to define all custom elements before any constructors are called?

I have two custom elements: One and Two. One has a function. Two has a child of One and tries to call that function in its constructor. It says that the function does not exist if I call customElements.define() on Two before One. However, if I define One before Two, it works just fine.
In my actual project, I do not have control of the order in which they are defined, and by default they are being defined in the wrong order.
I tried calling the function in the connectedCallback(), but this also failed.
When exactly is the constructor called?
Is there any way that I can make sure they are all defined before any constructors are called?
class One extends HTMLElement {
constructor() {
super()
console.log('one constructor')
}
myFunc() {
console.log('it worked!')
}
}
class Two extends HTMLElement {
constructor() {
super()
console.log('two constructor')
this.innerHTML = '<my-one></my-one>'
this.myOne = document.querySelector('my-one')
// this part fails
this.myOne.myFunc()
}
connectedCallback() {
// this also fails
this.myOne.myFunc()
}
}
// this works
// customElements.define("my-one", One)
// customElements.define("my-two", Two)
// this breaks
customElements.define("my-two", Two)
customElements.define("my-one", One)
<my-two></my-two>
It is all about the life-cycle of a Web Component and when a tag is upgraded from an HTMLUnknownElement to your actual Component
A component is defined in two steps.
1) Definition of a class
2) Calling customElements.define
Yes these two can be written together:
customElements.define('dog-food', class extends HTMLElement{});
But the class definition still happens before customElements.define is called.
Elements are only upgraded to a custom element when two things have happened:
1) The Custom Element must be defined by using customElements.define and
2) The Custom Element must either
1) be instantiated using document.createElement or new MyElement
2) be added to the DOM tree
This example has the element placed into the DOM but it is not defined for 1 second.
I display the constructor before it is defined and then, again, after it is defined.
class One extends HTMLElement {
constructor() {
super();
console.log('one constructor');
}
connectedCallback() {
this.innerHTML = "I have upgraded";
}
}
let el = document.querySelector('my-one');
setTimeout(() => {
customElements.define("my-one", One);
console.log('after: ', el.constructor.toString().substr(0,30));
}, 1000);
console.log('before: ', el.constructor);
<my-one>I am just a simple element</my-one>
In your code you use innerHTML to "load" <my-one>. But since <my-two> may not "really" be in the DOM by the time the constructor is called then the innerHTML will not be in the DOM and, thus, <my-one> will not get upgraded yet.
One thing you can do is to wait until the <my-two> component is really placed into the DOM by waiting to change the innerHTML in the connectedCallback function:
class One extends HTMLElement {
constructor() {
super();
console.log('one constructor');
}
myFunc() {
console.log('it worked!');
}
}
class Two extends HTMLElement {
constructor() {
super();
console.log('two constructor');
}
connectedCallback() {
this.innerHTML = '<my-one></my-one>';
setTimeout(() => {
this.myOne = document.querySelector('my-one');
this.myOne.myFunc();
});
}
}
customElements.define("my-two", Two)
customElements.define("my-one", One)
<my-two></my-two>
You will notice that I had to still place the call to the function in <my-one> into a setTimeout call. This is because the <my-one> element can not be upgraded until AFTER your connectedCallback function exists. Upgrading needs a chance to run and it will not run in the middle of your function.
Another way to do it is by calling the constructor for <my-one> directly:
class One extends HTMLElement {
constructor() {
super();
console.log('one constructor');
}
connectedCallback() {
this.innerHTML = "one";
}
myFunc() {
console.log('it worked!');
}
}
class Two extends HTMLElement {
constructor() {
super();
console.log('two constructor');
}
connectedCallback() {
customElements.whenDefined('my-one').then(() => {
this.myOne = document.createElement('my-one');
this.append(this.myOne);
this.myOne.myFunc();
});
}
}
customElements.define("my-two", Two);
customElements.define("my-one", One);
<my-two></my-two>
Here you will notice that I had to add a call to customElements.whenDefined. This will wait until <my-one> to actually be defined before it attempts to instantiate it. Once it is defined then you can create it and call the member function right away.
One last thing. There are rules for what you should and should not do while in a constructor for a Web Component. They are defined here https://w3c.github.io/webcomponents/spec/custom/#custom-element-conformance
But one thing I will point out is that you are not supposed to touch or change any attributes or child elements in the constructor. Mainly because there are no attribute or any child elements when the Web Component is constructed. Those are changed and added after the fact.

JavaScript Class Constructor - Is Constructor Called as Parent?

If I create the following classes, is there any way in Class1 to detect when the instance is actually one of Class2, without knowing anything about Class2?
i.e. Can Class1 tell when it's the parent class being extended?
class Class1 {
constructor() {
// Code to detect whether parent here
}
}
class Class2 extends Class1 {
constructor() {
super();
}
}
This is what new.target was made for - it gives you the constructor that new was invoked with. So
class Class1 {
constructor() {
if (new.target != Class1) {
// Class1 is used as a parent class
}
}
}

How to define a more customElements for same Class

I jsut tried to define new Custom Elements.
// the class
class MyEl extends HTMLElement {
constructor() {
// mandatory super call to upgrade the current context
super();
// any other Class/DOM related procedure
this.setAttribute('custom-attribute', 'value');
this.innerText = 'new element';
}
}
// its mandatory definition
customElements.define('my-el', MyEl);
// the JS way to create a custom element
const newMe = new MyEl();
document.body.appendChild(newMe);
Defining a new element of the same class will cause an error:
"Uncaught DOMException: Failed to execute 'define' on 'CustomElementRegistry': this constructor has already been used with this registry"
But i want to define new elements for same class.
Could you suggest a way to display multiple items from the same class (MyEl).
Thank in advance!
Just try with an anonymous constructor and extend your MyEl:
class MyEl extends HTMLElement {
// your definitions
}
customElements.define("my-better-element", class extends MyEl { })
customElements.define("my-second-element", class extends MyEl { })
customElements.define('my-el', MyEl) must be called only once to tell the browser you have a new <my-el> tag (aka to define a custom element).
Then you can create multiple elements just by using new:
const newMe1 = new MyEl();
const newMe2 = new MyEl();
Or createElement():
const newMe1 = document.createElement( 'my-el' )
const newMe2 = document.createElement( 'my-el' )
Or, in the html code:
<my-el></my-el>
<my-el></my-el>

Categories