Search code examples
javascriptweb-componentcustom-element

Failed to construct CustomElement


I get an issue using custom elements.

Error : Uncaught DOMException: Failed to construct 'CustomElement': The result must not have children

'use strict';

class TestCard extends HTMLDivElement {

  constructor() {
    super();
    this.headerNode = document.createElement('div');
    this.bodyNode = document.createElement('div');
    this.headerNode.className = 'card__header';
    this.bodyNode.className = 'card__body';
    this.appendChild(this.headerNode);
    this.appendChild(this.bodyNode);
  }

  connectedCallback() {
    this.classList.add('card');
  }

  static get observedAttributes() {
    return ['data-header', 'data-body'];
  }

  attributeChangedCallback(attrName, oldValue, newValue) {
    if (newValue !== oldValue) {
      this[attrName.replace('data-', '')] = newValue;
    }
  }

  set header(value) {
    this.headerNode.textContent = value;
    this.setAttribute('data-header', value);
  }

  set body(value) {
    this.bodyNode.innerHTML = value;
    this.setAttribute('data-body', value);
  }
}

customElements.define('test-card', TestCard, {
  extends: 'div'
});
<div is="test-card" data-header="Title" data-body="Content"></div>

Creating the WebComponent :

var cardNode = document.createElement('div');
cardNode.setAttribute('is', 'test-card');
cardNode.header = header;
cardNode.body = body;

Solution

  • Some things are not allowed in a custom element's constructor. For more info on this check an older answer by me to a similar question).

    Amongst others, those are:

    • accessing attributes (especially writing attributes, this includes class which is considered under control of the person consuming your component)
    • accessing children (neither read nor write)
      • unless you do that in the component's shadow tree.

    To achieve what you want to do, use shadow DOM:

    class TestComp extends HTMLElement {
      headerNode = document.createElement('div');
      bodyNode = document.createElement('div');
      
      constructor() {
        super();
        this.attachShadow({mode: 'open'});
        this.headerNode.className = 'card__header';
        this.bodyNode.className = 'card__body';
        this.bodyNode.part = 'body';
        this.shadowRoot.append(this.headerNode, this.bodyNode);
      }
      
      connectedCallback() {
        this.classList.add('card');
      }
    
      static get observedAttributes() {
        return ['data-header', 'data-body'];
      }
    
      attributeChangedCallback(attrName, oldValue, newValue) {
        if (newValue !== oldValue) {
          this[attrName.replace('data-', '')] = newValue;
        }
      }
    
      set header(value) {
        this.headerNode.textContent = value;
        this.dataset.header = value;
      }
    
      set body(value) {
        this.bodyNode.innerHTML = value;
        this.dataset.body = value;
      }
    }
    
    customElements.define('test-comp', TestComp);
    
    let newTestComp = new TestComp();
    newTestComp.header = 'FOOO';
    newTestComp.body = '<ul><li><i>BA</i><b>AA</b>R</ul>';
    document.body.append(newTestComp);
    test-comp::part(body) { color: green; }
    <test-comp data-header="Titre de ma carte" data-body="<h1>Test</h1>"></test-comp>

    Be aware that using shadow DOM means outside styles won't affect the styling of elements in the shadow tree. To apply styles to those, create a <style> element in the constructor, set it's textContent property to your styles, and append that next to your other elements in the shadow DOM.

    Instead of using a style element, you can also use Constructable Stylesheets. You'll probably need a polyfill because so far Chromium-based browsers are the only ones supporting it, but support is coming in other browsers (Firefox has had it for a while behind a flag: Open new tab, navigate to about:config and then set layout.css.constructable-stylesheets.enabled to true).

    To allow styling the component's inside from outside CSS, you can specify which elements are allowed to by styled from the outside using the part="name" attribute in your shadow DOM and then style it using the ::part(name) selector in CSS. Added that into the code example.