Search code examples
javascriptweb-componentnative-web-component

How can I specify the sequence of running nested web components constructors?


I created two web-components and nested one of them into the other. Both of them have its constructor. The problem that I have is that, I have no control on running the sequence of the constructors. Is there any way which I can set this out?

Here's my code:

child web component:

(function () {
    const template = document.createElement('template');
    template.innerHTML = `<div>WC1</div>`;

    class WC1Component extends HTMLElement {

        constructor() {
            super();
            console.log('WC1: constructor()');

            var me = this;

            me._shadowRoot = this.attachShadow({ 'mode': 'open' });
            me._shadowRoot.appendChild(template.content.cloneNode(true));
        }

        connectedCallback() {
            console.log('WC1: connectedCallback');
        }

        test() {
            console.log('test:wc1');
        }

    }

    window.customElements.define('wc-one', WC1Component);

}());

parent web component:

(function () {
    const template = document.createElement('template');
    template.innerHTML = `
    <wc-one id="wc1"></wc-one>
    <div>WC2</div>
    `;

    class WC2Component extends HTMLElement {

        constructor() {
            super();
            console.log('WC2: constructor()');

            var me = this;

            me._shadowRoot = this.attachShadow({ 'mode': 'open' });
            me._shadowRoot.appendChild(template.content.cloneNode(true));
            me._wc1 = me._shadowRoot.querySelector('#wc1');
        }

        connectedCallback() {
            console.log('WC2: connectedCallback');
            this._wc1.test(); // <-- Error: test is undefined!
        }


    }

    window.customElements.define('wc-two', WC2Component);

}());

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Test Web Component</title>
</head>
<body>
    <script src="wc1.js"></script>
    <script src="wc2.js"></script>

    <wc-two></wc-two>
    <script>
    </script>

</body>
</html> 

The result:

WC2: constructor()
WC2: connectedCallback

Uncaught TypeError: this._wc1.test is not a function

WC1: constructor()
WC1: connectedCallback

Solution

  • Actually you do have control on the running sequence of the constructors.

    Since <wc-one> is created by <wc-two> the WC2 constructor will always be called before WC1.

    The error is due to the fact that when you try invoke it, the inner component (WC1) is not yet added to the DOM.

    You could listen for the DOMContentLoaded event to be sure element is OK, or the load event on window, or implement the window.onload handler. @elanz-nasiri is aslo working.

    DOMContentLoaded will be fired first.

    There're alternate solution but they are more difficult to implement.

    window.customElements.define('wc-one', class extends HTMLElement {
        constructor() {
            super()
            this.attachShadow({ 'mode': 'open' }).innerHTML = `<div>WC1</div>`
        }
        test(source) {   
            console.log('test:wc1', source)
        }
    } )
    
    window.customElements.define('wc-two', class extends HTMLElement {
        constructor() {
            super()
            this._shadowRoot = this.attachShadow({ 'mode': 'open' })
            this._shadowRoot.innerHTML = `<wc-one id="wc1"></wc-one><div>WC2</div>`
            this._wc1 = this._shadowRoot.querySelector('#wc1');
        }
        connectedCallback() {
            setTimeout( ()=>this._wc1.test('setTimout') )            
            document.addEventListener( 'DOMContentLoaded', ()=>this._wc1.test('DOMContentLoaded') )
            window.onload = ()=>this._wc1.test('window.onload')
            window.addEventListener( 'load', ()=>this._wc1.test('load') )
        }
    } )
    <wc-two></wc-two>