Clarification regarding this.attachShadow({mode: 'open'}) and this.shadowRoot

Viewed 472

Regarding native WebComponents:

In the constructor() method at the start of the class it's conventional to attach a shadow DOM tree to the Custom Element and return a reference to its ShadowRoot:

class myCustomElement extends HTMLElement {

  constructor() {
    super();
    const shadowroot = this.attachShadow({mode: 'open'});
  }

 [... REST OF CODE HERE...]

}

The name of the reference can, of course, be anything.

I have seen:

  • var shadow = this.attachShadow({mode: 'open'}); (see: Element.attachShadow())
  • this.root = this.attachShadow({mode: 'open'});

by we could just as easily use:

  • const myShadowRootzzz = this.attachShadow({mode: 'open'});

However, there already exists Element.shadowRoot which is the ShadowRoot reference.

So why is the syntax

const shadow = this.attachShadow({mode: 'open'});

so commonly used in examples, when simply stating:

this.attachShadow({mode: 'open'});

would be sufficient, since... any time the ShadowRoot needs to be referenced, that reference is, literally:

this.shadowRoot 

Why would the WebComponent's ShadowRoot need another arbitrary reference beyond this.shadowRoot ?

Am I missing something important... or can I safely omit such additional references in my own WebComponents?

2 Answers

This is a great example where early code/blogs set the wrong example,
which every Web Component newbie then blindly copy/pastes.

Even the MDN documentation is wrong where it states: Use super() first

This is a perfectly valid constructor:

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, 
    // you just can't reference 'this' *before* it is created
    super() // create and return 'this'
        .attachShadow({ mode: "open" }) // create and return this.shadowRoot
        .append(myP);
  }

So a good reason to have a local variable is when one checks for a declarative shadow root.

class MyElement extends HTMLElement {
  constructor() {
    super();
    let internals = this.attachInternals();
    let shadow = internals.shadowRoot;

    //No declarative shadow root define, so attach
    if (!shadow) shadow = this.attachShadow({ mode: 'open' });

    shadow.addEventListener('slotchange', this.handleSlotChanged);
  }

  connectedCallback() {
    //Load template & content into shadowroot
  }

  handleSlotChanged(e) {
    //handle changes when slot changes
  }
}

customElements.define('my-element', MyElement);
<my-element>
  <template>
    <p>Nothing happens - no code to generate template</p>
  </template>
</my-element>
<my-element>
  <template shadowRoot="open">
    <p>Automatically generated due to declarative shadow root</p>
   </template>
</my-element>

The local "shadow" variable is assigned with "internals.shadowRoot", but that will be null if the template has no declarative shadow root, so assign it the normal way.

Finally, for both scenarios do something with the "shadow" no matter where it came from, e.g. attach some event handlers...

So one can use the this.shadowRoot in the final bit, but if this.shadowRoot is a property, it carries a little extra overhead. Also, is it really the same as "internals.shadowRoot"?

Related