Search code examples
javascriptweb-componentweb-component-testergoogle-web-componentnative-web-component

Web Component: Accessing the shadow DOM within the Custom Component class


With this project, I'm trying to create a custom component that will behave as a map. I'm still learning the fundamentals of Web Components through the W3C documentation and several Youtube videos.

The main question is, in the component class there's to be a function called attributeChangedCallback() that will be triggered every time one of the attributes change, these attributes are included in the observedAttributes() function and after setting them up (the previously mentioned function) I try to access the shadow DOM( that is declared connectedCallback() that will be triggered once the component is loaded in the HTML body element) through a selector. However, the variable content is null if I do it.

The attributeChangedCallback() should be loaded AFTER the content is loaded so I don't understand why this is happening and I need to access to this element every time the attributes change so I can update its content. Curious fact: if I log every time the attributeChangedCallback() is executed it log twice (because I have two attributes being watched).

Here's the snippet:

class GeoComponent extends HTMLElement {
	constructor() {
		super();
		this.shadow = this.createShadowRoot();
		this._latitude = 0;
		this._longitude = 0;
	}

	get latitude(){
		return this._latitude;
	}

	get longitude(){
		return this._longitude;
	}

	set latitude(val){
		this.setAttribute('latitude', val);
	}

	set longitude(val){
		this.setAttribute('longitude', val);
	}

	static get observedAttributes(){
		return ['latitude', 'longitude'];
	}
	
	attributeChangedCallback(name, oldVal, newVal){
		let geoComp = this.shadow.getElementById('geo');
		console.log(geoComp);
		switch(name){
			case 'latitude':
				this._latitude = parseInt(newVal, 0) || 0;
				// geoComp.innerHTML = `${this.latitude}, ${this.longitude}`;
				break;
			case 'longitude':
				this._longitude = parseInt(newVal, 0) || 0;
				// geoComp.innerHTML = `${this.latitude}, ${this.longitude}`;
				break
		}
	}

	connectedCallback(){
		let template = `
			<div class="geo-component" id="geo">
				
			</div>
		`;

		this.shadow.innerHTML = template;
	}
}

window.customElements.define('geo-component', GeoComponent);
<!DOCTYPE html>
<html>
<head>
	<title></title>
	<script src="geoComponent.js"></script>
</head>
<body>
	<h1>Geo-Component</h1>
	<geo-component latitude="12" longitude="-70"></geo-component>
</body>
</html>

UPDATE

Just like @acdcjunior mentioned after changing this.createShadowRoot(); to this.shadow = this.attachShadow({mode: 'open'}); (going from ShadowDOM v0 to v1) solved my problem given that the connectedCallback() function is executed behind scenes and only once.


Solution

  • attributeChangedCallback() is called when an (observed) attribute is initialized (declaratively, which is your case), added, changed or removed. Which means it is called before connectedCallback().

    Typically we use the constructor() to create the DOM:

    Name: constructor
    Called when: An instance of the element is created or upgraded. Useful for initializing state, settings up event listeners, or creating shadow dom. See the spec for restrictions on what you can do in the constructor.

    I moved your logic to it.

    Also you are using ShadowDOM v0. Updated it to v1 (attachShadow instead of createShadowRoot).

    Updated demo:

    class GeoComponent extends HTMLElement {
      constructor() {
        super();
        console.log('constructor called');
        
        this.attachShadow({mode: 'open'});
        this._latitude = 0;
        this._longitude = 0;
        
        let template = `
    			<div class="geo-component" id="geo">
    
    			</div>
    		`;
        this.shadowRoot.innerHTML = template;
      }
    
      get latitude() {
        return this._latitude;
      }
    
      get longitude() {
        return this._longitude;
      }
    
      set latitude(val) {
        this.setAttribute('latitude', val);
      }
    
      set longitude(val) {
        this.setAttribute('longitude', val);
      }
    
      static get observedAttributes() {
        return ['latitude', 'longitude'];
      }
    
      attributeChangedCallback(name, oldVal, newVal) {
        console.log('attributeChangedCallback() called:', name, ':', oldVal, '->', newVal);
    
        let geoComp = this.shadowRoot.getElementById('geo');
        console.log(geoComp);
        
        switch (name) {
          case 'latitude':
            this._latitude = parseInt(newVal, 0) || 0;
            // geoComp.innerHTML = `${this.latitude}, ${this.longitude}`;
            break;
          case 'longitude':
            this._longitude = parseInt(newVal, 0) || 0;
            // geoComp.innerHTML = `${this.latitude}, ${this.longitude}`;
            break
        }
      }
    
      connectedCallback() {
        console.log('connectedCallback() called');
      }
    }
    
    window.customElements.define('geo-component', GeoComponent);
    <!DOCTYPE html>
    <html>
    
    <head>
      <title></title>
      <script src="geoComponent.js"></script>
    </head>
    
    <body>
      <h1>Geo-Component</h1>
      <geo-component latitude="12" longitude="-70"></geo-component>
    </body>
    
    </html>