André Kuhlmann
André Kuhlmann

Reputation: 4668

Web Component "Cannot set property 'innerHTML' of null"

I have created a very basic custom element, that can change its value based on a provided attribute person. But whenever I'm loading my custom element I get this error: Cannot set property 'innerHTML' of null. When I add a breakpoint to the attributeChangedCallback function I can indeed see that on load the element is not there. When I continue loading though the element loads perfectly.

I could imagine because I'm using webpack to bundle all my files that the issue comes from loading the element at the end of the body instead of loading the element inside my head.

my-element.js:

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

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

  get person() {
     return this._name;
  }

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

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

  attributeChangedCallback(attrName, oldVal, newVal) {
     let myElementInner = this.shadow.querySelector('.my-element-inner');

     switch (attrName) {
        case 'person':
           this._person = newVal;

           // ======================
           // The error occures here
           // ======================
           myElementInner.innerHTML = `My name is ${this._person}`;

     }
  }

  connectedCallback() {
     var template =
     `
        <style>
        .my-element-inner {
           outline: blue dashed 1px;
           background-color: rgba(0,0,255,.1);
        }
        </style>
        <span class="my-element-inner">My name is ${this._person}</span>
     `

     this.shadow.innerHTML = template;
  }
}
customElements.define('my-element', MyElement);

index.html:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>WebPack Test Page</title>
</head>
<body>

  <my-element person="André"></my-element>

  <!-- Here goes the bundle.js -->
</body>
</html>

Upvotes: 1

Views: 1023

Answers (2)

ITEnthusiasm
ITEnthusiasm

Reputation: 106

Since the spec encourages pushing as much setup as possible to the connectedCallback, this might be preferable for some developers.

Note that attributeChangedCallback can still be called while the raw HTML is being parsed if any (observed) attributes are present on the element. For cases like these, I use a private #mounted property. This property is useful for preventing chunks of logic in connectedCallback from running redundantly (see the link to the spec above), but it can also be useful for preventing attributeChangedCallback from running prematurely.

class MyElement extends HTMLElement {
  #mounted = false;
  #person = '';

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

  get person() {
     return this.#person;
  }

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

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

  attributeChangedCallback(attrName, oldVal, newVal) {
     if (!this.#mounted) return;
     let myElementInner = this.shadow.querySelector('.my-element-inner');

     switch (attrName) {
        case 'person':
           this.#person = newVal;
           myElementInner.innerHTML = `My name is ${this.#person}`;
     }
  }

  connectedCallback() {
     const template = `
        <style>
        .my-element-inner {
           outline: blue dashed 1px;
           background-color: rgba(0,0,255,.1);
        }
        </style>
        <span class="my-element-inner">My name is ${this.#person}</span>
     `

     this.shadow.innerHTML = template;
     this.#mounted = true;
  }
}

customElements.define('my-element', MyElement);

Upvotes: 0

Hyyan Abo Fakher
Hyyan Abo Fakher

Reputation: 3527

The attributeChangedCallback() can be called before or after the connectedCallback depending on how your custom element is used.

If you move the connectedCallback logic to the constructor then things will be fine

Another option would be to check if myElementInner is null and keep your code in the connectedCallback

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

    this.shadow = this.attachShadow({mode: 'open'});
    this._person = '';
    var template =
      `
        <style>
        .my-element-inner {
           outline: blue dashed 1px;
           background-color: rgba(0,0,255,.1);
        }
        </style>
        <span class="my-element-inner">My name is ${this._person}</span>
     `

    this.shadow.innerHTML = template;
  }

  get person() {
    return this._person;
  }

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

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

  attributeChangedCallback(attrName, oldVal, newVal) {
    let myElementInner = this.shadow.querySelector('.my-element-inner');

    switch (attrName) {
      case 'person':
        this._person = newVal;
        if (myElementInner) {
          myElementInner.innerHTML = `My name is ${this._person}`;
        }

    }
  }
}
customElements.define('my-element', MyElement);
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>WebPack Test Page</title>
</head>

<body>

  <my-element person="André"></my-element>

  <!-- Here goes the bundle.js -->
</body>

</html>

Upvotes: 2

Related Questions