Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Create WebComponent through createElement

I'm having an issue creating a Web Component using createElement. I'm getting this error:

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

class TodoCard extends HTMLElement {
    constructor() {
        super()

        this.innerHTML = `
            <li>
                <div class="card">
                    <span class="card-content">${this.getAttribute('content')}</span>
                    <i class="fa fa-circle-o" aria-hidden="true"></i>
                    <i class="fa fa-star-o" aria-hidden="true"></i>
                </div>
            </li>
        `
    }
}

window.customElements.define('todo-card', TodoCard)

const todoList = document.getElementById('todo-list')
const todoForm = document.getElementById('todo-form')
const todoInput = document.getElementById('todo-input')

function appendTodo(content) {
    const todo = document.createElement('todo-card')
    todo.setAttribute('content', content)
    todoList.appendChild(todo)
}

todoForm.addEventListener('submit', e => {
    e.preventDefault()
    appendTodo(todoInput.value)
    todoInput.value = ''
})

any ideas? Thanks.

like image 777
ZeroSevenTen Avatar asked Jan 01 '26 06:01

ZeroSevenTen


1 Answers

A Custom Element (JSWC) that sets DOM content in the constructor
can never be created with document.createElement()

You will see many examples (including from me) where DOM content is set in the constructor.
Those Elements can never be created with document.createElement

Explanation (HTML DOM API):

When you use:

  <todo-card content=FOO></todo-card>

The element (extended from HTMLElement) has all the HTML interfaces (it is in a HTML DOM),
and you can set the innerHTML in the constructor

But, when you do:

  document.createElement("todo-card");

The constructor runs, without HTML interfaces (the element may have nothing to do with a DOM),
thus setting innerHTML in the constructor produces the error:

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

From https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-conformance:

The element must not gain any attributes or children, as this violates the expectations of consumers who use the createElement or createElementNS methods. In general, work should be deferred to connectedCallback as much as possible

shadowDOM is a DOM

When using shadowDOM you can set shadowDOM content in the constructor:

  constructor(){
    super().attachShadow({mode:"open"})
           .innerHTML = `...`;
  }

Correct code (for no shadowDOM!): use the connectedCallback:

<todo-card content=FOO></todo-card>

<script>
  customElements.define(
    "todo-card",
    class extends HTMLElement {
      constructor() {
        super();
        //this.innerHTML = this.getAttribute("content");
      }
      connectedCallback() {
        this.innerHTML = this.getAttribute("content");
      }
    }
  );

  try {
    const todo = document.createElement("todo-card");
    todo.setAttribute("content", "BAR");
    document.body.appendChild(todo);
  } catch (e) {
    console.error(e);
  }
</script>

You have another minor issue: content was a default attribute, and FireFox won't stop warning you:

Or don't use createElement

  const todo = document.createElement("todo-card");
  todo.setAttribute("content", "BAR");
  document.body.appendChild(todo);

can be written as:

  const html = `<todo-card content="BAR"></todo-card`;
  document.body.insertAdjacentHTML("beforeend" , html); 

The connectedCallback can run multiple times!

When you move DOM nodes around:

<div id=DO_Learn>
  <b>DO Learn: </b><todo-card todo="Custom Elements API"></todo-card>
</div>
<div id="DONT_Learn">
  <b>DON'T Learn!!! </b><todo-card todo="React"></todo-card>
</div>
<script>
  customElements.define(
    "todo-card",
    class extends HTMLElement {
      connectedCallback() {
        let txt = this.getAttribute("todo");
        this.append(txt);// and appended again on DOM moves
        console.log("qqmp connectedCallback\t", this.parentNode.id, this.innerHTML);
      }
      disconnectedCallback() {
        console.log("disconnectedCallback\t", this.parentNode.id , this.innerHTML);
      }
    }
  );
  const LIT = document.createElement("todo-card");
  LIT.setAttribute("todo", "Lit");
  DO_Learn.append(LIT);
  DONT_Learn.append(LIT);
</script>
  • connectedCallback runs for LIT
  • when LIT is moved
  • disconnectedCallback runs (note the parent! The Element is already in the new location)
  • connectedCallback for LIT runs again, appending "Learn Lit" again

It is up to you the programmer how your component/application must handle this

Web Component Libraries

Libraries like Lit, HyperHTML and Hybrids have extra callbacks implemented that help with all this.

I advice to learn the Custom Elements API first, otherwise you are learning a tool and not the technology.

And a Fool with a Tool, is still a Fool

Also read my Dev.to post on the connectedCallback: https://dev.to/dannyengelman/web-component-developers-do-not-connect-with-the-connectedcallback-yet-4jo7

like image 163
Danny '365CSI' Engelman Avatar answered Jan 03 '26 12:01

Danny '365CSI' Engelman



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!