1

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;
connexo
  • 53,704
  • 14
  • 91
  • 128
tonymx227
  • 5,293
  • 16
  • 48
  • 91
  • `this.getElementsByClassName('card__header')[0]` - at that point, your component doesn't have descendant nodes, so that should thrown an error. Please clarify what you are trying to achieve, and more specifically, why you are doing that in the constructor. The only place where you are free to append child nodes is in the component's shadow DOM - but yours doesn't have any. – connexo Apr 01 '22 at 14:59

3 Answers3

3

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.

connexo
  • 53,704
  • 14
  • 91
  • 128
  • Ok thank you it works, but is there any solution without adding a style element in the constructor to use my css class ? – tonymx227 Apr 04 '22 at 09:02
  • I've updated my post. You can see my my edit how I'm using the web component in my application. – tonymx227 Apr 04 '22 at 09:18
  • 1
    Adding the `is` attribute after creating the `div` won't have any effect. Use `const cardNode = document.createElement('div', { is: 'test-card' });` Also I cannot recommend extending `HTMLDivElement` as it a) won't work in Safari without a polyfill, and b) `HTMLDivElement` has no useful API that would make it worth extending anyway. – connexo Apr 04 '22 at 09:24
  • If the idea behind extending a div was to get a block level behaviour, instead add this CSS in your page: `test-card { display: block; }`. Please explain why you want to extend `HTMLDivElement`. – connexo Apr 04 '22 at 09:28
  • Thank you it works with the option in the `createElement` function. – tonymx227 Apr 04 '22 at 09:30
  • The last question, if I don't want to create the web component using JS but by adding the tag in my HTML document, is there any solution ? Like => `
    ` (with this code, it's not working).
    – tonymx227 Apr 04 '22 at 09:42
  • That code should work, but as mentioned, not in Safari. And again, don't extend div, use a custom tag instead. To check if your component is registered properly, select its tag in the element inspector, then switch to console with the element selected. There, enter `$0.constructor.name`. That should give you `TestCard`. – connexo Apr 04 '22 at 09:53
  • No, I get `Uncaught TypeError: Illegal constructor: localName does not match the HTML element interface at new testCard`. – tonymx227 Apr 04 '22 at 09:54
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/243575/discussion-between-connexo-and-tonymx227). – connexo Apr 04 '22 at 09:58
0

I cannot say about the error that you mentioned but there is one more thing which you should change.

Reorder these things in your code from

constructor() {
    super();
    this.classList.add('card');
    this.headerNode = this.getElementsByClassName('card__header')[0];
    this.bodyNode = this.getElementsByClassName('card__body')[0];
    this.innerHTML = `<div class="card__header"></div>
                      <div class="card__body"></div>`;
  }

to

constructor() {
    super();
    this.classList.add('card');
    this.innerHTML = `<div class="card__header"></div>
                      <div class="card__body"></div>`;
    this.headerNode = this.getElementsByClassName('card__header')[0];
    this.bodyNode = this.getElementsByClassName('card__body')[0];

  }

Since you are adding the element with classes card__header and card__body later in the code, And you are trying to access it that is why this.headerNode is undefined.

tikeswar101
  • 126
  • 1
  • 5
  • Still violating *the result must not have children*. – connexo Apr 01 '22 at 15:04
  • I ran your code on Stackblitz and it is working fine there. You could try this as well to create the component and setting attribute ```const newTextComp = document.createElement('test-comp'); newTextComp.setAttribute('data-header', 'Food'); newTextComp.setAttribute('data-body', '
    • BAAAR
    '); document.body.appendChild(newTextComp); ```
    – tikeswar101 Apr 01 '22 at 15:59
-1

As I commented in Connexo his older answer.
You have to understand the Web Components lifecycle: https://andyogo.github.io/custom-element-reactions-diagram/

When you run new TestComp() or document.createElement("test-comp")
there is NO DOM Element, only the constructor runs, and tells you,
you can't add DOM (children) to it (nothing). Only in the connectedCallback can you work with existing DOM, anything inside or after your <test-comp> might not exist yet; see: wait for Element Upgrade in connectedCallback: FireFox and Chromium differences

And please optimize that constructor,
any blog or answer showing this.shadowRoot inside the constructor should be burned

  constructor() {
    super() // sets AND returns "this" scope
     .attachShadow({mode:'open'}) // sets AND returns shadowRoot
     .append(
        this.headerNode = document.createElement('div'),
        this.bodyNode = document.createElement('div')
     );
    this.headerNode.className = 'card__header';
    this.bodyNode.className = 'card__body';
  }
E_net4
  • 27,810
  • 13
  • 101
  • 139
Danny '365CSI' Engelman
  • 16,526
  • 2
  • 32
  • 49