0

I'm trying to make somewhat of a reusable header for a website (multiple pages, can't be bothered to copy/paste everything); inside it I only have a logo-element element (custom as well) whose id I want to make equal to the header's plus a __logo suffix.

Here's the javascript:

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

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

        let logoElement = document.createElement("logo-element");
        logoElement.setAttribute("id", `${ this.getAttribute("id") }__logo`);

        this.shadowRoot.append(logoElement); 
    }
}

customElements.define('top-bar-element', TopBar);

And here's the HTML:

<top-bar-element id="top-bar"></top-bar-element>

Yet, when I check, I find <logo-element id="null__logo"></logo-element>.

I suppose it is because the browser only sets the attribute after the creation of the element.

Is there any other explanation? Are there workarounds?

lenerdv
  • 177
  • 13

1 Answers1

2

You are correct, in the constructor phase the element is only in memory, can't access the DOM

So you have to do it in the connectedCallback:

customElements.define('my-element', class extends HTMLElement {
  constructor() {
    let content = ["slot","logo-element"].map(x=>document.createElement(x));
    super() // docs are wrong, super doesn't have to be first
      .attachShadow({mode: "open" })
      .append( ...content );
    this.id = "TWO";
  }
  connectedCallback() {
    this  
      .shadowRoot
      .querySelector("logo-element")
      .id = this.id + "__logo";

    console.log(this.shadowRoot.innerHTML);
  }
});
<my-element id="ONE">Hello World!</my-element>

No constructor

But you do not need the constructor in your own element (the default one from HTMLElement will be executed)

customElements.define('my-element', class extends HTMLElement {
  connectedCallback() {
    this.attachShadow({mode: "open"}).innerHTML = `<slot></slot>`;
    let el = this.shadowRoot.appendChild(document.createElement("logo-element"));
    el.id = this.id + "__logo";
    console.log(this.shadowRoot.innerHTML);

    let host = this.shadowRoot.getRootNode().host;
    console.log(host==this , host); 
  }
});
<my-element id="TWO">Hello World!</my-element>

Notes:

  • showing different uses to set element HTML content, you can mix what works for you
  • id is a default attribute (like title and name) and (by default) have a Getter/Setter
  • your logo-element works, but still is an UNKNOWN Element
  • Docs should say: You can not use the 'this' scope reference before super() is called
  • shadowDOM is not required; you can create a Custom Element without

Without shadowDOM

customElements.define('my-element', class extends HTMLElement {
  connectedCallback() {
    this.innerHTML = `<logo-element id="${this.id}__logo">${this.innerHTML}</logo-element>`;
    console.log(this.innerHTML);
  }
});
<my-element id="THREE">Hello World!</my-element>
Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49
  • Thanks! I used the shadow DOM mainly for the same reason (browser was complaining about using innerHTML when the element wasn't initialized yet); btw, I'm far from a js guru, so could you please explain why you created a slot element and what the map() function does? – lenerdv Jan 01 '21 at 10:31
  • SLOTs are one of the major concepts to bring content into shadowDOM. [see this long answer](https://stackoverflow.com/questions/61626493/slotted-css-selector-for-nested-children-in-shadowdom-slot/61631668#61631668). Javascripts map converts an Array into another array; in the above code it turns 2 strings into 2 DOM elements. Good practice is to always search with the "MDN" word in Google, so you get the correct/best JavaScript documentation.. Play and learn, that will take time. – Danny '365CSI' Engelman Jan 01 '21 at 15:50
  • Related; when I try to *assign* `this.id` in the constructor (after calling `super()`) it seems to work, but when the constructor returns I get `Uncaught DOMException: Operation is not supported` and there is no id on the returned element - which is kind of annoying because internally I would like to be able to reference the id even before the element is actually attached to the document - it's possible to set the id of an element before inserting it into the DOM, so why can't I do it in the constructor? – Michael May 28 '22 at 02:00
  • Few pointers to add - retrieving id of the Custom Element itself in connected callback needed me to do the following -> this.id = this.shadowRoot.getRootNode().host.getAttribute("id"); ---- For future reference – Gautam Feb 20 '23 at 03:33
  • @Gautam, see my second SO snippet. ``this`` is the same as the ``shadowRoot.getRootNode().host`` call – Danny '365CSI' Engelman Feb 20 '23 at 08:16
  • 1
    @Michael, That is because ``id`` is both a property **and** an attribute. ``this.id="FOO"`` can not set the attribute (as the element is not in DOM yet) You can set any other property you want in the constructor – Danny '365CSI' Engelman Feb 20 '23 at 08:44