Embedding a custom HTMLElement into another custom HTMLElement - javascript

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

Related

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 reuse a class method defined in one class which can be used in another class in typescript?

So, I'm new to Typescript, and recently started learning from documentation. I was looking at its documentation and there were no signs of reusing method from other class.
Class A file
export class A{
... constuctor(){
const queue = this.createQueue()
}
createQueue(){
console.log("Has access to this", +this);
}
}
And there is a class B with exact same definations and uses the same method. How do I make this reusable so that, both of them call with "this"?
One solution I thought of is to create a seperate helper Class that could I use, but I'm not sure how to do that.
Any thoughts?
In this case, inheritance would probably be your best bet. This basically means that you can extend upon another class and include all of the other one's properties/getters/setters/methods.
// we denote this class as abstract because it must be extended and cannot be directly initialized, i.e. new BaseFoo()
abstract class BaseFoo<T> { // `T` is the item type within the queue
queue: T[];
constructor() {
this.queue = this.createQueue();
}
createQueue() {
// can access `this`
return [];
}
}
class FooA extends BaseFoo<number> {
constructor() {
super(); // runs child class's constructor
console.log(this.queue); // []
}
}
class FooB extends BaseFoo<string> {
constructor() {
super();
console.log(this.queue); // []
}
}
TypeScript Playground Link

Disadvantage of building a custom element within the constructor?

I understand the advantages of templates in terms of performance when designing custom elements, but for structures which are only used in one element I am struggling to understand the disadvantage of building the html within the constructor() of the element class definition itself.
Put another way, what is the disadvantage of doing something like this:
const myTemplate = document.createElement("template");
myTemplate.innerHTML = `<p>my text</p>`;
customElements.define( 'my-elem', class extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(myTemplate.content.cloneNode(true));
}
})
over this:
customElements.define( 'my-elem', class extends HTMLElement {
constructor() {
super();
let myP = document.createElement("p");
let myText = document.createTextNode("my text");
myP.appendChild(myText);
this.attachShadow({ mode: "open" });
this.shadowRoot.appendChild(myP);
}
})
... when the latter option both (a) preserves the advantages of using createElement, and (b) helps prevent potential encapsulation issues introduced by defining the template outside the scope of the element definition?
I'm also aware that I could build the template with createElement instead of innerHTML in the example above, but that has the potential to introduce even more variables defined outside the scope of the element definition.
It is a subtle difference and boils down to requirements and/or team/personal preference.
I would do:
customElements.define( 'my-elem', class extends HTMLElement {
constructor() {
let myP = document.createElement("p");
let myText = document.createTextNode("my text");
myP.append(myText);
// MDN docs are wrong, you can put code *before* super
super() // create and return 'this'
// create and return this.shadowRoot
.attachShadow({ mode: "open" })
.append(myP);
}
})
customElements.define( 'my-elem2', class extends HTMLElement {
constructor() {
super()
.attachShadow({ mode: "open" })
.innerHTML = "Hello! Component";
}
})
<my-elem></my-elem>
<my-elem2></my-elem2>

Order of Custom Elements Execution

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>

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