Search code examples
javascriptsortingcheckboxstylinglit-element

Litelement - Table sorting renders wrong values


I am trying to sort an HTML table based on it's values in my Lit element. However, I'm running into a problem with my component. Here is an overview of my table; enter image description here

The problem

In this application, you need to be able to sort on every table header. However, items which are considered 'done' need to move to the bottom of the table. My problem arises whenever I mark an item as done. In the following example I will mark the top todo (task: 123) as done. The expected behaviour is that the todo is moved to the bottom of the table with it's checkbox enabled. This is not however what is the outcome at the moment.

enter image description here

As you can see, the todo item with task 123 is moved to the bottom. However, the todo with task 456 also gets it's checkbox marked. This is not desired behaviour and I don't know what's causing it. You can also see that the colors are not correct (this is some styling to show you that a changed todo is being saved, yellow = saving, green = saved and red = error).

Things I have tried

Since I don't know what is exactly causing this issue I don't know what I should do. I gave all my inputs/rows/td's id's to make sure nothing gets mixed up, but that doesn't seem to work.

Code

import { LitElement, html, css } from 'lit-element';

class TableList extends LitElement {
    static get properties() {
        return {
            data: {
                type: Array
            },
            primaryKey: {
                type: String
            },
            defaultSortKey: {
                type: String
            }
        };
    }

    set data(value) {
        let oldValue = this._data;
        this._data = [ ... value];
        this.sortByHeader();
        this.requestUpdate('data', oldValue);
    }

    get data() {
        return this._data;
    }
    
    async edit(entry, key, event) {
        this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.remove('saved');
        this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.remove('error');
        this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.add('saving');

        if (entry[key].type === "checkbox") {
            entry[key].value = event.target.checked;
        } else {
            entry[key].value = event.target.value;
        }

        if (await update(entry)) {
            this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.remove('saving');
            this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.add('saved');

            setTimeout(() => {
                this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.remove('saved');
            }, 1000);
        } else {
            this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.remove('saving');
            this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.add('error');

            setTimeout(() => {
                this.shadowRoot.getElementById('entry' + entry[this.primaryKey].value).classList.remove('error');
            }, 5000);
        }
    }

    sortByHeader(key) {       
        if (key === undefined) {
            key = this.defaultSortKey;
        }

        let oldValue = this.data;
        this._data = [ ... this.data.sort((a, b) => {
            return a[this.defaultSortKey].value - b[this.defaultSortKey].value 
                        || a[key].value - b[key].value;
        })];

        this.requestUpdate('data', oldValue);
    }

    renderHeaders() {
        let keys = Object.keys(this.data[0]);

        return keys.map(key => html`
            ${this.data[0][key].visible ?
                html`
                    <th id="${'header' + key}" @click="${() => this.sortByHeader(key)}">
                        ${key}
                    </th>
                `: ''
            }
        `)
    }

    renderRows() {
        return this.data.map(entry => html`
        <tr id="${'entry' + entry[this.primaryKey].value}">
            ${Object.keys(entry).map(key => html`
                ${entry[key].visible && !entry[key].editable ?
                    html`<td>${entry[key].value}</td>`
                    : ``
                }
                ${entry[key].visible && entry[key].editable ?
                    html`<td id="${'td' + key + entry[this.primaryKey].value}">
                            <input
                                id="${'input' + key + entry[this.primaryKey].value}"
                                name="${'input' + key + entry[this.primaryKey].value}"
                                type="${entry[key].type}"
                                ?checked="${entry[key].value}"
                                value="${entry[key].value}"
                                @change="${(event) => {
                                    this.edit(entry, key, event)
                                }}"
                            />
                        </td>`
                    : ``
                }
            `)}
        </tr>
        `)
    }

    render() {
        return html`
            <table id="table-list">
                <thead>
                    <tr>
                        ${this.renderHeaders()}
                    </tr>
                </thead>
                <tbody>
                    ${this.renderRows()}
                </tbody>
            </table>
      `;
    }

    static get styles() {
        return css`
            table {
                width: 100%;
                border-collapse: collapse;
                font-family: Arial, Helvetica, sans-serif;
            }

            th {
                padding-top: 12px;
                padding-bottom: 12px;
                text-align: center;
                background-color: #4CAF50;
                color: white;
            }

            tr {
                text-align: right;
                -moz-transition: all .2s ease-in;
                -o-transition: all .2s ease-in;
                -webkit-transition: all .2s ease-in;
                transition: all .2s ease-in;
                background: white; 
                padding: 20px;
            }

            .disabled {
                color: lightgrey;
            }

            .saving {
                background: yellow;
            }

            .saved {
                background: lightgreen;
            }

            .error {
                background: red;
            }

            .sort:after {
                content: ' ↓';
            }
        `;
    }
}

export default TableList;

Solution

    1. If you are using array with id in template use repeat function of lit-html

      import { repeat } from "lit-html/directives/repeat";

    2. For styling don't add classes to DOM manually. Use classMap from lit-html

      import { classMap } from "lit-html/directives/class-map";

    I fixed your checkbox issue please follow this link.

    For more information about lit-html please refer to their documentation.