0

I'm trying to build an interactive tree-like component using Lit, and I'm running into some issues when trying to remove tree nodes. Here's the implementation of the component:

import { html, LitElement } from 'https://cdn.jsdelivr.net/gh/lit/dist@2/all/lit-all.min.js';

let counter = 0;

export class TreeNode extends LitElement {
    static properties = {
        label: { type: String },
        children: { state: true }
    };

    constructor() {
        super();
        this.children = [];
    }

    render() {
        return html`
            <div>
                ${this.label}
                <button @click=${this.onAddButtonClick}>add child</button>
                <button @click=${this.onRemoveButtonClick}>remove</button>
            </div>
            <ul>
                ${this.children.map(child => html`
                    <li>
                        <tree-node label=${child.label} @remove=${(ev) => { ev.stopPropagation(); this.removeChild(child.id); }}></tree-node>
                    </li>
                `)}
            </ul>
        `;
    }

    onAddButtonClick() {
        const id = counter++;
        this.children = [...this.children, { id, label: `Node #${id}` }];
    }

    onRemoveButtonClick() {
        this.dispatchEvent(new Event('remove', { bubbles: true, composed: true }));
    }

    removeChild(id) {
        this.children = this.children.filter(child => child.id !== id);
    }
}

customElements.define('tree-node', TreeNode);

export class TreeView extends LitElement {
    render() {
        return html`<tree-node label="root"></tree-node>`;
    }
}

customElements.define('tree-view', TreeView);

And a working example is here: https://codepen.io/petrbroz/pen/wvyzzPp

Now, let's say I have the following tree structure:

- Node A
  - Node B
    - Node C
    - Node D
    - Node E
  - Node F

Now, if I try to remove Node B, its children are not removed, but instead they become children of Node F. Why is that? Am I doing something wrong in the implementation? I've been able to work around this by manually passing the children to nested nodes as shown below, but I'd like to understand why the original implementation isn't working. Thanks!

import { html, LitElement } from 'https://cdn.jsdelivr.net/gh/lit/dist@2/all/lit-all.min.js';

let counter = 0;

export class TreeNode extends LitElement {
    static properties = {
        label: { type: String },
        children: { type: Array }
    };

    constructor() {
        super();
        this.children = [];
    }

    render() {
        return html`
            <div>
                ${this.label}
                <button @click=${this.onAddButtonClick}>add child</button>
                <button @click=${this.onRemoveButtonClick}>remove</button>
            </div>
            <ul>
                ${this.children.map(child => html`
                    <li>
                        <tree-node label=${child.label} .children=${child.children} @remove=${(ev) => { ev.stopPropagation(); this.removeChild(child.id); }}></tree-node>
                    </li>
                `)}
            </ul>
        `;
    }

    onAddButtonClick() {
        const id = counter++;
        this.children = [...this.children, { id, label: `Node #${id}`, children: [] }];
    }

    onRemoveButtonClick() {
        this.dispatchEvent(new Event('remove', { bubbles: true, composed: true }));
    }

    removeChild(id) {
        this.children = this.children.filter(child => child.id !== id);
    }
}

customElements.define('tree-node', TreeNode);

export class TreeView extends LitElement {
    render() {
        return html`<tree-node label="root"></tree-node>`;
    }
}

customElements.define('tree-view', TreeView);
Petr Broz
  • 8,891
  • 2
  • 15
  • 24

1 Answers1

0

If anyone runs into this as well, here's a solution: use the repeat directive to ensure that each node is given its unique key.

For the original implementation in this question, the TreeNode class would look like this:

import { LitElement, css, html, repeat } from 'https://cdn.jsdelivr.net/gh/lit/dist@2/all/lit-all.min.js';

let counter = 0;

export class TreeNode extends LitElement {
    static properties = {
        label: { type: String },
        children: { state: true }
    }

    constructor() {
        super();
        this.children = [];
    }

    render() {
        return html`
            <div>
                ${this.label}
                <button @click=${this.onAddButtonClick}>add child</button>
                <button @click=${this.onRemoveButtonClick}>remove</button>
            </div>
            <ul>
                ${repeat(this.children, (child) => child.id, (child) => html`
                    <li>
                        <tree-node label=${child.label} @remove=${(ev) => { ev.stopPropagation(); this.removeChild(child.id); }}></tree-node>
                    </li>
                `)}
            </ul>
        `;
    }

    onAddButtonClick() {
        const id = counter++;
        this.children = [...this.children, { id, label: `Node #${id}` }];
    }

    onRemoveButtonClick() {
        this.dispatchEvent(new Event('remove', { bubbles: true, composed: true }));
    }

    removeChild(id) {
        this.children = this.children.filter(child => child.id !== id);
    }
}

customElements.define('tree-node', TreeNode);
Petr Broz
  • 8,891
  • 2
  • 15
  • 24