Search code examples
javascripthtmlweb-componentcustom-element

attributeChangedCallback doesn't get called even after defining observedAttributes


In the following code, attributeChangedCallback never gets called even if the 'content' attribute is created, changed or removed.

class Square extends HTMLElement {

    static get observedAttributes() {

        return ['content'];
    }
    constructor(val) {
        super();
        console.log('inside constructor');
        this.attachShadow({mode: 'open'});
        this.shadowRoot.appendChild(document.createElement('button'));

        this.button = this.shadowRoot.querySelector('button');
        this.button.className = "square";

        this.content = val;

        console.log('constructor ended');
    }

    get content() {
        console.log('inside getter');
        return this.button.getAttribute('content');
    }
    set content(val) {
        console.log('setter being executed, val being: ', val);
        // pass null to represent empty square
        if (val !== null) {
            this.button.setAttribute('content', val);

        } else {
            if (this.button.hasAttribute('content')) {
                this.button.removeAttribute('content');
            }
        }

    }
    connectedCallback() {
        //console.log('connected callback being executed now');
    }

    // not working :(
    attributeChangedCallback(name, oldValue, newValue) {
        console.log('attribute changed callback being executed now');
        if (name === 'content') {
            this.button.innerHTML = newValue?newValue:" ";
        }
    }
}
customElements.define('square-box', Square); 

Based on the best practices given here, I want the side-effect of attribute change (updation of innerHTML in my case) to take place in attributeChangedCallback. However when I move this updation to the setter, the code works fine.


Solution

  • set content(val) {
            console.log('setter being executed, val being: ', val);
            // pass null to represent empty square
            if (val !== null) {
                this.button.setAttribute('content', val);
    
            } else {
                if (this.button.hasAttribute('content')) {
                    this.button.removeAttribute('content');
                }
            }
    
        }
    

    You are mixing up parents & children

    You are defining a setter on your Element <square-box> (►parent element)

    When you do this.button.setAttribute('content', val);

    You are changing a attribute of your <button> Element (►child element)

    This will never trigger the attributeChangedCallback of (►parent) <square-box>
    because its attributes were not changed

    You either have to go "up the DOM" with .getRootNode() and/or .host to set attributes of parent elements.

    or use Custom Events (bubbling up the DOM) to notify parents that children have done/changed something

    I presume you meant to do

    set content(val) {
            //  loose ==null comparison for null AND undefined, 
            //  element.content=null; will remove the attribute
            if (val==null) 
                this.removeAttribute('content');
            else 
            //  but you DO want .content(0) (0==false) set as "0"
                this.setAttribute('content', val);
        }