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

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.

Related

I'm trying to add to an array of objects that is broken into two inputs in React

So I have an array of objects where the keys are 'cost' and 'service' called estimate. You can add to the array by clicking 'Add' which adds a new index (i) to the array. The issue is on the first cycle I get a good array of {'cost': 2500, 'service': "commercial cleaning"} (imgSet-1) but when I add another item it completely erases the array and sets only one of the nested objects key and value. (imgSet-2). This is the outcome I'm looking for once the state has been saved (imgSet-3) I have tried going with #RubenSmn approach but then I receive this error. (imgSet-4)
imgSet-1 *********
Adding an initial service
Outcome of the initial service addition
imgSet-2 *********
Adding the second service
Outcome of the second service addition
imgSet-3 *********
imgSet-4 *********
Below is the code for the part of the page where you can add services and the output of the text inputs.
const [estimate, setEstimate] = useState([]);
{[...Array(numServices)].map((e, i) => {
return (
<div key={i} className="flex justify-between">
<div>
<NumericTextBoxComponent
format="c2"
name={`cost-${i}`}
value={estimate?.items?.["cost"]?.[i]}
change={(e) =>
setEstimate({ ...estimate, items: [{...estimate?.items?.[i],cost: e?.value}]})
}
placeholder='Price'
floatLabelType="Auto"
data-msg-containerid="errorForCost"
/>
</div>
<div>
<DropDownListComponent
showClearButton
fields={{ value: "id", text: "service" }}
name={`service-${i}`}
value={estimate?.items?.["service"]?.[i]}
change={(e) =>
setEstimate({ ...estimate, items: [{...estimate?.items?.[i],service: e?.value}]})
}
id={`service-${i}`}
floatLabelType="Auto"
data-name={`service-${i}`}
dataSource={estimateData?.services}
placeholder="Service"
data-msg-containerid="errorForLead"
></DropDownListComponent>
<div id="errorForLead" />
</div>
</div>
);
})}
</form>
<button onClick={() => setNumServices(numServices + 1)}>Add</button>
I have tried multiple variations of spread operators but I can't seem to get it to work. My expected result would be:
estimate:{
items: [
{'cost': 2500, 'service': 'Commercial Clean'},
{'cost': 500, 'service': 'Bathroom Clean'},
{'cost': 180, 'service': 'Apartment Clean'},
{etc.}
]
}
The initial state is an array which is not the object you're setting in the change handlers. You can have an initial state like this.
const [estimate, setEstimate] = useState({ items: [] });
You're not adding back the old items of the state when you're setting the new state.
setEstimate({
...estimate,
items: [{ ...estimate?.items?.[i], cost: e?.value }],
// should be something like
// items: [...estimate.items, { ...estimate.items?.[i], cost: e?.value }],
});
But you can't do that since it will create a new object in your items array every time you change a value.
I made this dynamic handleChange function which you can use for you state changes. The first if statement is to check if the itemIndex is already in the items array. If not, create a new item with the propertyName and the value
const handleChange = (e, itemIndex, propertyName) => {
const newValue = e?.value;
setEstimate((prevEstimate) => {
if (prevEstimate.items.length <= itemIndex) {
const newItem = { [propertyName]: newValue };
return {
...prevEstimate,
items: [...prevEstimate.items, newItem]
};
}
// loop over old items
const newItems = [...prevEstimate.items].map((item, idx) => {
// if index' are not the same just return the old item
if (idx !== itemIndex) return item;
// else return the item with the new service
return { ...item, [propertyName]: newValue };
});
return {
...prevEstimate,
items: newItems,
};
});
};
For the Service dropdown, you can do the same for the Cost just change the property name
<DropDownListComponent
...
value={estimate.items[i]?.service}
change={(e) => handleChange(e, i, "service")}
...
></DropDownListComponent>
See here a simplified live version

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([]);
}
});
},

LitElement maintain internal state

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.

State set method did not effect the main object - ReactJS

I have three components in my project, App, DataTable and Table.
The App will render a DataTable that contains the Table component. I just called DataTable with data and columns props in it.
// data sample
const tmpData = [
{
name: "Can",
age: 4,
}, {
name: "David",
age: 44,
}, {
name: "Sara",
age: 14,
}, {
name: "Hani",
age: 24,
}
]
// main columns array
const tmpColumns = [
{
title: "Name",
accessor: "name",
}, {
title: "Age",
accessor: "age",
}
]
function App() {
return (
<div className="App" style={{background: "#f0f0f0", padding: "1rem"}}>
<div>App Component:</div>
<DataTable data={tmpData} columns={tmpColumns}/>
</div>
);
}
DataTable component is just for handling selection and filter actions on my table. So, it could manipulate the data and the columns. the Table will render in it and here is the code of DataTable.
function DataTable(props) {
const [data, setData] = useState(props.data)
const [columns, setColumns] = useState(props.columns)
const [selection, setSelection] = useState([])
useEffect(() => {
// add select columns after component mount
handleColumnChange()
}, [])
// listen to selection change
useEffect(() => {
// selection change log, It change on each select.
console.log(selection);
}, [selection])
function handleRowSelect(rowName) {
const keyIndex = selection.indexOf(rowName);
console.log(selection, rowName, keyIndex);
if (keyIndex === -1)
setSelection(preSelection => ([...preSelection, ...[rowName]]))
else
setSelection(preSelection => preSelection.filter(sl => sl !== rowName))
}
function handleColumnChange() {
// add select column if not added already
if (!columns.find(col => col.accessor === 'select')) {
setColumns([
...[{
title: "Select",
accessor: "select",
// this method will execute to render checkbox on Select table
Cell: function (row) {
return <input type="checkbox"
onChange={() => handleRowSelect(row.name, selection)}
checked={selection.includes(row.name)}/>
},
}],
...columns,
])
}
}
return (
<div className="DataTable" style={{background: "#e0e0e0", padding: "1rem"}}>
<div>Data Table:</div>
<Table {...{data, columns}}/>
</div>
)
}
Table component will render columns and suitable data for them. For each column in columns array we have an item to access data (accessor) or an executable method to return custom data (Cell) and here is its code.
function Table(props) {
const [data, setData] = useState(props.data)
return (
<div className="Table" style={{background: "#d5d5d5", padding: "1rem"}}>
<div>Table</div>
<table>
<tbody>
<tr>
{props.columns.map((th, key) => (
<th key={key}>{th.title}</th>
))}
</tr>
{/* generating data rows */}
{data.map((dr, key) => (
<tr key={key}>
{columns.map((col, index) => (
<td key={index}>
{
// the "Cell" method has high priority than "accessor" selector
(col.Cell && typeof col.Cell === "function") ?
col.Cell(dr) :
dr[col.accessor]
}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}
As you saw above, to handle row selection I manipulate the columns in the DataTable component by adding a new column at first index of my columns array. Everything works fine for now. But, when I try to select a row, the Call methods of the select column, could not access the selection array state of my DataTable component. and it's my problem!
Actually, on each select, the selection array must update and target checkbox must check. Apparently, there is a copy of the selection that changes (or maybe not changes).
You also could check the whole project on CodeSandbox
I have some fixes to your code, check it out on CodeSandbox.
The idea is that you don't need to put the columns to state, instead you just need to get the columns with the selection box.
I also added a simple optimization to it by implementing React.useMemo to memoized the calculated columns. It will only be re-calculated when the props.columns and selection state changes.
Hope this helps! Happy coding! :)
Ok, your tmpData passed from App to DataTable is read-only. So by design you will not see any change your data along the way.
There're couple of ways to get it working, mostly having something to do to allow your DataTable to pass the change back to App if that happens.
Step 1, you could add one prop called onRowClick on the DataTable,
<DataTable data={tmpData} onRowClick={row => { console.log(row) } />
Step 2, you need to allow your tmpData to change after the event. You are using hooks, so we can
const [tmpData, setTmpData] = useState([sampleData])
return (
<DataTable data={tmpData} onRowClick={row => {
// use setTmpData to adjust the tmpData to get a new instance of tmpData
} />
)
Of course for things with this complexity, we normally use useReducer instead of useState, since there's definitely other things that you want to do other than selecting the row :)

How to design a generic filter like ecommerce website have using ReactJs?

i am planning to build a generic filter like Gbif Have.
My question is how to approach this problem.
I like to use ReactJs for this project.
What other technology i need to look into along with React and redux in order to design such a generic filter.
I try to design this filter using React and redux only.
In my approach, i try to maintain the query parameter inside the state variable of the get_data method, in which i am fetching the data from the server. As somebody click on any filter button, then i pass custom event from that filter component along with query parameter and handle this event in get_data method. In get_data method again i am saving this value in get_data state parameter and again getting the new filtered data.
Now the Problem with above approach is that as the number of parameter increases it become very difficult to maintain.
my get_data constructor look like this.
constructor(props){
super(props);
this.state={
params:{
max:10,
offset:0,
taxon:[],
sGroup:[],
classification:undefined,
userGroupList:[],
isFlagged:undefined,
speciesName:undefined,
isMediaFilter:undefined,
sort:"lastRevised",
webaddress:""
},
title:[],
groupName:[],
userGroupName:[],
view:1
}
this.props.fetchObservations(this.state.params)
this.loadMore=this.loadMore.bind(this);
};
The way i am getting data from filter component is something like this.
this is my handleInput method which fire onSelect method from one of the filter.
handleInput(value,groupName){
this.setState({
active:true
})
this.props.ClearObservationPage();
var event = new CustomEvent("sGroup-filter",{ "detail":{
sGroup:value,
groupName:groupName
}
});
document.dispatchEvent(event);
};
the way i am handling this event in my get_data component is look something like this.
sGroupFilterEventListner(e){
const params=this.state.params;
if(!params.sGroup){
params.sGroup=[];
}
console.log("params.sGroup",params.taxon)
params.sGroup.push(e.detail.sGroup)
params.sGroup=_.uniqBy(params.sGroup)
const groupName=this.state.groupName;
var titleobject={};
titleobject.sGroup=e.detail.sGroup;
titleobject.groupName=e.detail.groupName;
groupName.push(titleobject);
let newgroupname=_.uniqBy(groupName,"sGroup")
params.classification=params.classification;
let isFlagged=params.isFlagged;
let speciesName=params.speciesName;
let MediaFilter=params.isMediaFilter;
let taxonparams=params.taxon;
taxonparams= taxonparams.join(",");
let sGroupParams=params.sGroup;
sGroupParams=sGroupParams.join(",");
let userGroupParams=params.userGroupList;
userGroupParams=userGroupParams.join(",");
let newparams={
max:10,
sGroup:sGroupParams,
classification:params.classification,
offset:0,
taxon:taxonparams,
userGroupList:userGroupParams,
isFlagged:isFlagged,
speciesName:speciesName,
isMediaFilter:MediaFilter,
sort:params.sort
}
this.props.fetchObservations(newparams);
this.setState({
params:{
max:10,
sGroup:params.sGroup,
classification:params.classification,
offset:0,
taxon:params.taxon,
userGroupList:params.userGroupList,
isFlagged:isFlagged,
speciesName:speciesName,
isMediaFilter:MediaFilter,
sort:params.sort
},
groupName:newgroupname
})
}
I registered and unRegistered the sGroupFilterEventListner in my componentDidMount and componentunmount method.
Presently i am also not considering the case where if somebody type in url bar, the filter panel change automatically.
Please consider all the above scenario and suggest me a generic way to do the same. thanks.
My Current Filter Panle look like this
Here's a quick example (React only, no Redux) I whipped up with a dynamic number of filters (defined in the filters array, but naturally you can acquire that from wherever).
const filters = [
{ id: "name", title: "Name", type: "string" },
{
id: "color",
title: "Color",
type: "choice",
choices: ["blue", "orange"],
},
{
id: "height",
title: "Height",
type: "choice",
choices: ["tiny", "small", "big", "huge"],
},
{
id: "width",
title: "Width",
type: "choice",
choices: ["tiny", "small", "big", "huge"],
},
];
const filterComponents = {
string: ({ filter, onChange, value }) => (
<input
value={value || ""}
onInput={e => onChange(filter.id, e.target.value)}
/>
),
choice: ({ filter, onChange, value }) => (
<select
value={value || ""}
onInput={e => onChange(filter.id, e.target.value)}
size={1 + filter.choices.length}
>
<option value="">(none)</option>
{filter.choices.map(c => (
<option value={c} key={c}>
{c}
</option>
))}
</select>
),
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = { filters: {} };
this.onChangeFilter = this.onChangeFilter.bind(this);
}
onChangeFilter(filterId, value) {
const newFilterState = Object.assign({}, this.state.filters, {
[filterId]: value || undefined,
});
this.setState({ filters: newFilterState });
}
renderFilter(f) {
const Component = filterComponents[f.type];
return (
<div key={f.id}>
<b>{f.title}</b>
<Component
filter={f}
value={this.state.filters[f.id]}
onChange={this.onChangeFilter}
/>
</div>
);
}
render() {
return (
<table>
<tbody>
<tr>
<td>{filters.map(f => this.renderFilter(f))}</td>
<td>Filters: {JSON.stringify(this.state.filters)}</td>
</tr>
</tbody>
</table>
);
}
}
ReactDOM.render(<App />, document.querySelector("main"));
body {
font: 12pt sans-serif;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<main/>
(originally on https://codepen.io/akx/pen/JyemQQ?editors=0010)
Hope this helps you along.

Categories

Resources