LitElement maintain internal state - javascript

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
);

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:
cache the series and checked state when data updates, instead of on each render
use hidden attr to hide unchecked series
use data-attributes to pass serializable data on collection items to event listeners.
use attribute: false instead of type: Array (assuming you don't need to deserialize data from attributes.

Related

Indeterminate checkbox not working when filtered React MUI-Datatables

Info
I have a project that is using React, Redux, and MUI-Datatables. A simple demo for this project can be found at this CodeSandbox.
In this app, there are two main components, a map and a datatable. The two communicate via redux so that when a row is selected in the table, the respective circle in the map is highlighted and vice versa.
Problem
My problem is with the indeterminate toggle selectAll checkbox on the table. When the user has selected a row then applies a filter, the selectAll checkbox shows the '-' indeterminate symbol, but nothing happens when it is clicked.
Steps to recreate:
User selects the first row in the table, circle1.
User opens filter dialog in right-hand corner of table.
From the Marker dropdown menu in the filter dialog, User selects circle3 as the filter value.
User closes filter dialog
User clicks on selectAll checkbox at the top of the select row column. It will be showing the '-' symbol.
Notice that nothing changes. No rows are selected or deselected.
Desired Behavior:
When the User has selected a row in the table then applies a filter, the selectAll checkbox should still select all visible rows on first click and deselect all on second click the same way it normally would.
Code
Live: CodeSandbox
Table Component:
import React, { useEffect, useState } from "react";
import MUIDataTable from "mui-datatables";
import { connect } from "react-redux";
import { handleSelection } from "./redux";
import circles from "./assets/data/circles";
import { addToOrRemoveFromArray } from "./utils";
// Table component
const Table = ({ handleSelection, selections }) => {
const [selectionIndexes, setSelectionIndexes] = useState([]);
// When 'selections' changes in redux store:
useEffect(() => {
let indexes = [];
// Iterate selections:
selections.forEach((selection) => {
// Push the index of the selected
// circle into index arr:
let index = circles.indexOf(selection);
indexes.push(index);
});
// Set selections to local state hook:
setSelectionIndexes(indexes);
}, [selections]);
// Table options:
const options = {
rowsSelected: selectionIndexes, // User provided array of numbers (dataIndexes) which indicates the selected rows
selectToolbarPlacement: "none",
selectableRows: "multiple", // Enable selection of multiple rows
setRowProps: (row, dataIndex, rowIndex) => {
return {
style: {
padding: ".5rem",
margin: ".5rem auto"
}
};
},
// When a row(s) is/are selected:
onRowSelectionChange: (
currentRowsSelected,
allRowsSelected,
rowsSelected
) => {
let temp = [];
let indexes = [];
// Iterate rowsSelected:
rowsSelected.forEach((row) => {
// Add or remove row index to/from indexes arr:
indexes = addToOrRemoveFromArray(row, indexes, "indexes");
// Circle data:
let circle_data = circles[row];
// Add or remove circle_data to/from temp arr:
temp = addToOrRemoveFromArray(circle_data, temp, "temp");
});
// Set indexes to local hook:
setSelectionIndexes(indexes);
// Send the circle data to redux:
handleSelection(temp);
}
};
const columns = [
{
name: "marker",
label: "Marker",
options: {
filter: true,
sort: false
}
},
{
name: "lat",
label: "Latitude",
options: {
filter: true,
sort: false
}
},
{
name: "lon",
label: "Longitude",
options: {
filter: true,
sort: false
}
},
{
name: "radius",
label: "Radius",
options: {
filter: true,
sort: false
}
}
];
const table_name = `Circle Markers`;
return (
<>
<div style={{ display: "table", tableLayout: "fixed", width: "100%" }}>
<MUIDataTable
title={<h3>{table_name}</h3>}
data={circles}
columns={columns}
options={options}
/>
</div>
</>
);
};
const mapStateToProps = (state) => {
return {
selections: state.selection.selections
};
};
const mapDispatchToProps = (dispatch) => {
return {
handleSelection: (selections) => dispatch(handleSelection(selections))
};
};
export default connect(mapStateToProps, mapDispatchToProps)(Table);
How can I get the selectAll checkbox to work properly when a row outside of the filtered data has been selected?
Is it ok to de-select the selected row when filters applied? I did a workaround to meet the desired behavior.
Live Code: CodeSandBox
I added additional code in Table.jsx line 34
onFilterChange: (changedColumn, changedColumnIndex, displayData) => {
changedColumnIndex.forEach((data, key) => {
if (Array.isArray(data) && data.length) {
setSelectionIndexes([]);
}
});
},

How to show/hide React element of fetched array.map()

I want to show/hide a part of JSX depending on onClickShowChart state property and output the correct chart data based on the ID.
But this part inside a map loop, when i click to show element then it will fetch the data based on the ID then return back to array and show the chart.
Challenge:
The problem is every time i click to show the chart, every mapped items will shown up the same chart data, because it's depending on the same state property. I can't set individual state because it is using array.map() function to loop all records.
How do I show/hide the correct chart data individually without effect and preserve other record state and chart data?
constructor(props) {
super(props);
// Initial states
this.state = { dataList[], showChart: false, showChartData: [] }
}
componentWillmount() {
this._getDataList()
}
_getDataList() {
axios.get(`.../List`,
{
params: { id: id },
headers: { 'Authorization': ...accessToken }
}).then((res) => {
this.setState({ dataList: res.data })
})
})
onClickShowChart = (id) => {
this.setState({ showChart: true }, () => this._getGraphData(id))
}
// When click to show, it will fetch graph data and then pass to state
_getGraphData(id) {
axios.get(`.../productAdsStatistic`,
{
params: { id: id },
headers: { 'Authorization': ...accessToken }
}).then((res) => {
this.setState({ graphData: res.data })
})
})
renderChart() {
return (
<Chart data={this.state.graphData}>
// ...
</Chart>
)
}
render() {
return (
<div>
<Row>
<Col>
{this.state.dataList.map((v) => {
<h1>{v.title}<h1>
<span onClick={() => this.onClickShowChart(v._id)}>
Click to show chart
</span>
<Row>
<Col>{this.state.showChart === true ? renderChart() : ''}</Col>
</Row>
}
</Col>
</Row>
</div>
}
}
JSON Array result from API
[
{
_id: C1,
title: Chart A
},
{
_id: C2,
title: Chart B
}
]
Graph Data JSON Array result from API for 1 chart
[
{
month: "Jan",
value: 7
},
{
month: "Feb",
value: 6.9
}
]
Follwing is the sandbox link:
https://codesandbox.io/s/ancient-snow-ne0gv?file=/src/DataList.js
Class version of the above solution:
https://codesandbox.io/s/tender-dream-xy3vm
Expanding the idea whatever I have mentioned in comments: Just mantain a separate state variable which will store the indices of item in dataList which got clicked. and renderChart should accept one argument corresponding to the rowIndex. in renderChart function check the rowIndex exists in above state indices array, if it's there, render the chart, else null.

LitElement remove item from list

When the page first loads, the delete buttons generated by the code below work as expected. However, if you alter the text in one of the <textarea> elements, the delete button no longer works correctly. How can I fix this?
import { LitElement, html } from 'lit-element';
class MyElement extends LitElement {
static get properties() {
return {
list: { type: Array },
};
}
constructor() {
super();
this.list = [
{ id: "1", text: "hello" },
{ id: "2", text: "hi" },
{ id: "3", text: "cool" },
];
}
render() {
return html`${this.list.map(item =>
html`<textarea>${item.text}</textarea><button id="${item.id}" #click="${this.delete}">X</button>`
)}`;
}
delete(event) {
const id = event.target.id;
this.list = this.list.filter(item => item.id !== id);
}
}
customElements.define("my-element", MyElement);
I'm not sure of the exact cause, but I think it has to do with the way lit-html decides which DOM elements to remove when rendering a list with fewer items than the previous render. The solution is to use the repeat directive. It takes as its second argument a function that helps lit-html identify which DOM elements correspond to which items in the array:
import { repeat } from 'lit-html/directives/repeat.js'
// ...
render(){
return html`
${repeat(this.list, item => item.id,
item => html`<textarea>${item.text}</textarea><button id="${item.id}" #click="${this.delete}">X</button><br>`
)}
`;
}

How would I change the background color of a list item upon click? (ES6 & Polymer)

I've cloned a repository which focuses on creating a To-Do application using ES6 and Polymer 3. I'm trying to implement a button which turns the background color containing a string green upon click. I've tried doing this, but I keep failing to get the desired result.
Example code:
static get properties() {
return {
list: {type: Array},
todo: {type: String},
};
}
constructor() {
super();
this.list = [
this.todoItem('buy cereal'),
this.todoItem('buy milk')
];
this.todo = '';
this.createNewToDoItem = this.createNewToDoItem.bind(this);
this.handleKeyPress = this.handleKeyPress.bind(this);
this.handleInput = this.handleInput.bind(this);
}
todoItem(todo) {
return {todo}
}
createNewToDoItem() {
this.list = [
...this.list,
this.todoItem(this.todo)
];
this.todo = '';
}
//Right here is where I tried to implement the background color change.
checkItem() {
checkItem = document.getElementById('checkItem'),
checkItem.addEventListener('click', () => {
this.list = this.list.filter(this.todo)
document.body.style.backgroundColor = 'green';
});
}
deleteItem(indexToDelete) {
this.list = this.list.filter((toDo, index) => index !== indexToDelete);
}
render() {
return html`
${style}
<div class="ToDo">
<h1>Grocery List</h1>
<h1 class="ToDo-Header">What do I need to buy today?</h1>
<div class="ToDo-Container">
<div class="ToDo-Content">
${repeat(
this.list,
(item, key) => {
return html`
<to-do-item
item=${item.todo}
.deleteItem=${this.deleteItem.bind(this, key)}
></to-do-item>
`;
}
)}
</div>
I'd be eternally thankful if someone helped me out. I've created two JSFiddle links which show the code I've worked on thus far:
Link 1: https://jsfiddle.net/r2mxzp1c/ (Check line 42-49)
Link 2: https://jsfiddle.net/zt0x5u94/ (Check line 13 & 22-24)
I'm not sure about the approach. But this link might help you
https://stackblitz.com/edit/web-components-zero-to-hero-part-one?file=to-do-app.js
from this guy: https://stackblitz.com/#thepassle
You should try to make the reactive templating work for you by defining presentation details in terms of your element's properties.
For example, this is a stripped-down approach to the same problem:
class TestElement extends LitElement{
static get properties() {
return {
'items': { 'type': Array }
};
}
constructor() {
super();
// set up a data structure I can use to selectively color items
this.items = [ 'a', 'b', 'c' ].map((name) =>
({ name, 'highlight': false }));
}
render() {
return html`<ol>${
this.items.map((item, idx) =>
html`<li
#click="${ () => this.toggle(idx) }"
style="background: ${ item.highlight ? '#0f0' : '#fff' }">
${ item.name }
</li>`)
}</ol>`;
}
toggle(idx) {
// rendering won't trigger unless you replace the whole array or object
// when using properties of those types. alternatively, mutate with the
// usual .push(), .splice(), etc methods and then call `this.requestUpdate()`
this.items = this.items.map((item, jdx) =>
jdx === idx ? { ...item, 'highlight': !item.highlight } : item
);
}
}
https://jsfiddle.net/rzhofu81/305/
I define the template such that the elements are colored the way I want depending on an aspect of their state (the "highlight" attribute of each entry in the list), and then I focus the interaction on updating the state to reflect what the user is doing.

Ember - nested recursive modules

I am working on an click and drop feature --- where on the page the module is used in a recursive way so it has a parent and children.
I have hit an issue where if the user started to select the children - and then selects the parent - I want to deselect the children. Although I am unsure how to store or monitor a change in the parent/child items selected to make global deselection.
So the user has selected the child of bacon3.. if they select the parent - it would need to deselect the children -- but I feel I am currently locked in the scope of the module
I think this example will help you https://canary.ember-twiddle.com/468a737efbbf447966dd83ac734f62ad?openFiles=utils.tree-helpers.js%2C
So, this was an interesting problem. It turned out to be more of a recursion problem than anything having to do with ember, javascript, or checkbox behavior.
Here is what I have (using the updated syntax and such (if you have the option to upgrade to 3.4, you most definitely should -- it's a dream))
// wrapping-component.js
import Component from '#ember/component';
import { action, computed } from '#ember-decorators/object';
import { check } from 'twiddle/utils/tree-helpers';
export default class extends Component {
options = [{
id: 1,
label: 'burger',
checked: false,
children: [{
id: 3,
label: 'tomato',
checked: false
}, {
id: 4,
label: 'lettus',
checked: false
}, {
id: 5,
label: 'pickle',
checked: false
}]
}, {
id: 2,
label: 'kebab',
checked: false,
children: [{
id: 6,
label: 'ketchup',
checked: false
}, {
id: 7,
label: 'chilli',
checked: false
}]
}];
#action
toggleChecked(id) {
const newTree = check(this.options, id);
this.set('options', newTree);
}
}
template:
{{yield this.options (action this.toggleChecked)}}
and the usage:
// application.hbs
<WrappingComponent as |options toggle|>
{{#each options as |item|}}
<CheckboxGroup #item={{item}} #onClick={{toggle}} />
{{/each}}
</WrappingComponent>
CheckboxGroup is a template-only component:
// checkbox-group.hbs
<div class="checkboxhandler">
<input
type="checkbox"
checked={{#item.checked}}
onclick={{action #onClick #item.id}}
>
<label>{{#item.label}}</label>
{{#if #item.children}}
{{#each #item.children as |child|}}
<CheckboxGroup #item={{child}} #onClick={{#onClick}} />
{{/each}}
{{/if}}
</div>
and the recursive helpers (this is a mess, but I've just been prototyping):
// utils/tree-helpers.js
const toggle = value => !value;
const disable = () => false;
// the roots / siblings are contained by arrays
export function check(tree, id, transform = toggle) {
if (tree === undefined) return undefined;
if (Array.isArray(tree)) {
return selectOnlySubtree(tree, id, transform);
}
if (tree.id === id || id === 'all') {
return checkNode(tree, id, transform);
}
if (tree.children) {
return checkChildren(tree, id, transform);
}
return tree;
}
function selectOnlySubtree(tree, id, transform) {
return tree.map(subTree => {
const newTree = check(subTree, id, transform);
if (!newTree.children || (transform !== disable && didChange(newTree, subTree))) {
return newTree;
}
return disableTree(subTree);
});
}
function isTargetAtThisLevel(tree, id) {
return tree.map(t => t.id).includes(id);
}
function checkNode(tree, id, transform) {
return {
...tree,
checked: transform(tree.checked),
children: disableTree(tree.children)
};
}
function disableTree(tree) {
return check(tree, 'all', disable);
}
function checkChildren(tree, id, transform) {
return {
...tree,
checked: id === 'all' ? transform(tree.checked) : tree.checked,
children: check(tree.children, id, transform)
};
}
export function didChange(treeA, treeB) {
const rootsChanged = treeA.checked !== treeB.checked;
if (rootsChanged) return true;
if (treeA.children && treeB.children) {
const compares = treeA.children.map((childA, index) => {
return didChange(childA, treeB.children[index]);
});
const nothingChanged = compares.every(v => v === false);
return !nothingChanged;
}
return false;
}
hope this helps.

Categories

Resources