Search code examples
javascriptweb-componentshadow-domcustom-elementnative-web-component

Using connectedCallback() breaks the web component


I am learning web components and am building a dynamic list to get the hang of them. Having got it working, I read that it's best to attach the shadow root using the connectedCallback method. However, having tried to do this, I get a bunch of errors I can't fix. Also, The way I am setting a simple attribute to a label seems a bit long winded: is there a simpler way just to pick up an attribute and display it as a label?

This is my working example:

const template = document.createElement('template');
template.innerHTML = `
<style>
    :host {
    display: block;
    font-family: sans-serif;
    text-align: center;
    }

    button {
    border: none;
    cursor: pointer;
    }

    ul {
    list-style: none;
    padding: 0;
    }
</style>
<h1>To dos</h1>

<lable id="lable1"></lable>
<select></select>
`;

class TodoApp extends HTMLElement {
    constructor() {
        super();

        this._shadowRoot = this.attachShadow({ 'mode': 'open' });
        this._shadowRoot.appendChild(template.content.cloneNode(true));
        this.$todoList = this._shadowRoot.querySelector('select'); 
        this.label1 = this._shadowRoot.getElementById('lable1')

    }

    static get observedAttributes() {
      return ['att1'];
    }

    attributeChangedCallback(name, oldValue, newValue) {
      this.label1.innerText = this.getAttribute('att1');
    }

    renderTodoList() {
        this.$todoList.innerHTML = '';

        this.todosArray.forEach((todoP) => {
            let $todoItem = document.createElement('option');
            $todoItem.text = todoP.text; 
            $todoItem.value = todoP.id; 
            this.$todoList.appendChild($todoItem);
        });
    }

    set todos(value) {
        this.todosArray = value;
        this.renderTodoList();
    }

}

window.customElements.define('to-do-app', TodoApp);

When I add a connectedCallback() method and create the shadow dom there, I get a bunch of errors. My markup is just:

<to-do-app att1="value 1 attribute"></to-do-app>

I tried this:

class TodoApp extends HTMLElement {
    constructor() {
        super();
    this.label1 = '';
    }

    connectedCallback() {
        this._shadowRoot = this.attachShadow({ 'mode': 'open' });
        this._shadowRoot.appendChild(template.content.cloneNode(true));
        this.$todoList = this._shadowRoot.querySelector('select'); 
        this.label1 = this._shadowRoot.getElementById('lable1')

    }

    static get observedAttributes() {
      return ['att1'];
    }

    attributeChangedCallback(name, oldValue, newValue) {
      this.label1.innerText = this.getAttribute('att1');
    }

But get the error:

TypeError: can't assign to property "innerText" on "": not an object


Solution

  • I'm not sure at all it's best to define the Shadow DOM in connectedCallback() (unless you want to work with a Shadow DOM polyfill. Where did you read that?

    Anyway, if your example with connectedCallback(), the error is due to the fact that attributeChangedCallback() is invoked before connectedCallback().

    That's why the property this.label1 is not set yet when attributeChangeCallback() is called.

    Instead, test the property existence:

    attributeChangedCallback(name, oldValue, newValue) {
        if ( this.label1 )
            this.label1.innerText = this.getAttribute('att1');
    }
    

    And, when rendering the component, test the attribute existence:

    connectedCallback() {
        //...
        this.label1 = this._shadowRoot.getElementById('lable1')
        if ( this.getAttribute( 'att1' ) )
            this.label1.innerText = this.getAttribute( 'att1' ) 
    }
    

    Update

    The best way to read an attribute depends on when you need it. In your use case, bacause it is already in the markup when you need it in connectedCallback(), you can just get it by using this.getAttribute().

    Tu assign its value, maybe you should use a template literal with variable instead of a <template> element.

    let label = this.getAttribute( 'att1' )
    this.shadowRoot.innerHTML = `<label>${label}</label>`