0

I am trying to build a chart with LitElement. The chart takes a data property from the user, and displays this data (the chart plot). It also gets series names from the data, in order to display a legend with a checkbox for each series that can be used to show or hide the data for that series on the chart plot.
The below is a very minimal example where the chart plot is simply divs containing the data points (3, 5, 4, 7), and the legend is just checkboxes. The expected behaviour is that when a checkbox is selected/deselected, the corresponding data in the chart plot (data divs) is shown/hidden. For example, initially both checkboxes are selected by default, and the data for both series is correctly display. However, if I deselect the first checkbox, I expect the data for "series1" to be hidden, so only 5 and 7 are displayed.
It is this checkbox behaviour that I cannot get working. When I select or deselect a checkbox, I log this.series which seems to be correctly updated reflect which checkboxes are selected, however the chart plot (data divs) is not updated.

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

class TestElement extends LitElement {
  static get properties() {
    return {
      data: { type: Array },
      series: { type: Array },
    };
  }

  constructor() {
    super();
    this.data = [];
    this.series = [];
  }
  checkboxChange(e) {
    const inputs = Array.from(this.shadowRoot.querySelectorAll("input")).map(n => n.checked);
    this.series = this.series.map((s, i) => ({ ...s, checked: inputs[i] }));
    console.log("this.series", this.series);
  }
  render() {
    this.series = Object.keys(this.data[0]).map(key => ({ key, checked: true }));
    const data = this.data.map(d => this.series.map(s => (s.checked ? html`<div>${d[s.key]}</div>` : "")));
    const series = this.series.map(
      s => html`<input type="checkbox" ?checked=${s.checked} @change=${this.checkboxChange} />`
    );
    return html`${data}${series}`;
  }
}
customElements.define("test-element", TestElement);

render(
  html`<test-element
    .data=${[
      { series1: "3", series2: "5" },
      { series1: "4", series2: "7" },
    ]}
  ></test-element>`,
  window.document.body
);
Max888
  • 3,089
  • 24
  • 55
  • Your code looks weird and most probably has a bug. Especially this line: ``` this.series = Object.keys(this.data[0]).map(key => ({ key, checked: true })); ``` where you set checked: true - regardless of the checkbox state. – toto11 Jun 29 '20 at 09:31
  • Take a look at https://stackoverflow.com/questions/55962214/litelement-not-updating-checkbox-in-list/55994717#55994717 – Umbo Jun 30 '20 at 22:51

1 Answers1

2

Try the following:

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

class TestElement extends LitElement {
  static get properties() {
    return {
      data: { attribute: false, accessors: false },
      series: { attribute: false, accessors: false },
      checked: { attribute: false, accessors: false },
    };
  }

  constructor() {
    super();
    this.data = [];
    this.series = new Map();
    this.checked = new Map();
  }

  get data() {
    return this.__data || [];
  }

  set data(v) {
    const oldValue = this.__data;
    this.__data = Array.isArray(v) ? v : [];
    this.series = new Map();

    for (const row of this.data) {
      for (const [series, value] of Object.entries(row)) {
        this.series.set(series, [...this.series.get(series) || [], value])
      }
    }

    for (const series of this.series.keys()) {
      this.checked.set(series, this.checked.get(series) ?? true);
    }

    this.requestUpdate('data', oldValue);
    this.requestUpdate('series', null);
    this.requestUpdate('checked', null);
  }

  checkboxChange(e) {
    this.checked.set(e.target.dataset.series, e.target.checked);
    this.requestUpdate('checked', null);
  }

  render() {
    return [
      [...this.series.entries()].map(([series, values]) => values.map(value => html`
        <div ?hidden="${!this.checked.get(series)}">${value}</div>
      `)),
      [...this.checked.entries()].map(([series, checked]) => html`
        <input type="checkbox" ?checked=${checked} data-series="${series}" @change=${this.checkboxChange} />
      `)
    ];
  }
}

customElements.define("test-element", TestElement);

Live Example: https://webcomponents.dev/edit/FEbG9UA3nBMqtk9fwQrD/src/index.js

This solution presents a few improvements:

  1. cache the series and checked state when data updates, instead of on each render
  2. use hidden attr to hide unchecked series
  3. use data-attributes to pass serializable data on collection items to event listeners.
  4. use attribute: false instead of type: Array (assuming you don't need to deserialize data from attributes.
Benny Powers
  • 5,398
  • 4
  • 32
  • 55