Search code examples
javascripthtmllit-htmlhtmlelements

Nested HTMLElement rendered with lit-html overwrites parent's template


I have two minimal HTMLElements: an AppRoot and a SubElement. The innerHTML of the elements is generated via lit-html's render and html templating functions.

AppRoot's HTML template is a div with two paragraphs in it: one displays the text of a message attribute, the other one instantiates a SubElement and passes it a string.

The SubElement's HTML template is solely the passed string.

I would expect the rendered HTML to look like this:

<div>
    <p>AppRoot's message</p>
    <p>The string passed to SubElement</p>
</div>

But it is in fact only the SubElement's rendered template:

The string passed to SubElement

Why does the SubElement's template substitute AppRoot's template when it's rendered? I've experimented with changing the tags (enclosing with divs, paragraphs), but to no avail.

You can find the source code below.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="./index.js" type="module"></script>
    <app-root></app-root>
</body>
</html>
import { render, html } from './node_modules/lit-html/lit-html.js';


class SubElement extends HTMLElement {

    constructor(message) {
        super();
        this.message = message;
    }

    connectedCallback() {
        this.innerHTML = render(this.template(), document.body);;
    }

    template() {
        return html`
            ${this.message}
        `;
    }

}

customElements.define('sub-element', SubElement);


class AppRoot extends HTMLElement {

    constructor() {
        super();
        this.message = "I am root!";
    }

    connectedCallback() {
        this.innerHTML = render(this.template(), document.body);
    }

    template() {
        return html`
            <div>
                <p>${this.message}</p>
                <p>${new SubElement("I am not root!")}</p>
            </div>
        `;
    }

}

customElements.define('app-root', AppRoot);

Solution

  • The second parameter of render is the container where the template should be rendered at. Each component is currently rendering the template results to the document body and overwriting previously rendered results.

    connectedCallback() {
        this.innerHTML = render(this.template(), document.body);
    }
    

    You should look into using shadow DOM or render within the component itself.

    connectedCallback() {
        render(this.template(), this);
    }