I have got a grid where in a column can have array of items. All I need is to implement a solution where in If that column has more than 1 items, need to display "Show more" and on click of it should show all the items(comma separated) and bring a "Show Less " link that would hide all items except the first one .Also when there is no data , just show "Not Available" for that column value. I am using react-table for grid purposes
I have tried the following : https://codesandbox.io/s/react-table-row-table-mpk9s
import * as React from "react";
import { render } from "react-dom";
import DataGrid from "./DataGrid";
import ShowMore from "./ShowMore";
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
columns: [],
};
}
componentDidMount() {
this.getData();
this.getColumns();
}
showMoreUtility = arr => {
return <ShowMore value={arr} />;
};
getData = () => {
const data = [
{ firstName: "Jack", status: "Submitted", items: [1, 2, 3, 4] },
{ firstName: "Simon", status: "Pending", items: [1, 2] },
{ firstName: "Syls", status: "Pending", items: [1, 2] },
{ firstName: "Pete", status: "Approved", items: [] }
];
this.setState({ data });
};
getColumns = () => {
const columns = [
{
id: "1",
Header: "First Name",
accessor: "firstName"
},
{
id: "2",
Header: "Status",
accessor: "status"
},
{
id: "3",
Header: "Items",
accessor: arr => this.showMoreUtility(arr.items)
}
];
this.setState({ columns });
};
render() {
return (
<>
<DataGrid
data={this.state.data}
columns={this.state.columns}
/>
</>
);
}
}
Based on the code from the linked sandbox you could add a Cell property to the items column and pass the value to your ShowMore component:
{
Header: "Items",
accessor: "items",
Cell: row => (
<ShowMore value={row.value} />
)
}
then in your ShowMore component add || !value.length to your condition in order to return Not Available when there is no data
if (value === undefined || value === null || !value.length) {
return 'Not Available';
}
Also add a onClick event to the div to update the value of isTruncated and change the displayed data:
function handleClick() {
truncate(!isTruncated);
}
return (
<div onClick={handleClick}>
{
isTruncated
? <span>
{value[0]}
{value.length > 1 && ' Show more'}
</span>
: <span>
{value.join(',')}
{value.length > 1 && ' Show less'}
</span>
}
</div>
);
Working example:
You're almost there. The ShowMore component needs some modifications and the items column isn't correct either.
I wrote an example of a working ShowMore component to display how this can be done:
const ShowMore = props => {
const { value } = props;
const [isTruncated, setTruncate] = useState(true);
const toggleTruncate = () => setTruncate(!isTruncated);
if (value === undefined || value === null) {
return null;
}
if (value.length === 0) {
return 'Unavailable'
}
return (
<div>
{isTruncated ? value[0] : value}
<span onClick={toggleTruncate}>
{isTruncated ? "Show more" : "Show less"}
</span>
</div>
);
};
When modifying the ShowMoreItem component like this it will work, but according to the specs of react-table this isn't the correct way to use it. The accessor attribute should be used to retrieve the correct data instead of rendering a custom component. For custom rendering you can use the Cell attribute.
Modify the items columns like following:
accessor: "items", // This will get the 'items' attribute from the row.
Cell: row => {
// This will render the ShowMore component with the correct value.
return <ShowMore value={row.value} />;
}
Related
I have defined a state variable called fullText.
fullText default value is false.
When Full Text is clicked, I want to reverse my state value and enable the text to be closed and opened. But when it is clicked, the texts in all rows in the table are opened, how can I make it row specific?
{
!this.state.fullText ?
<div onClick={() => this.setState({ fullText: !this.state.fullText })}
className="text-primary cursor-pointer font-size-14 mt-2 px-2"
key={props.data?.id}>
Full Text
</div>
:
<>
<div onClick={() => this.setState({ fullText: !this.state.fullText
})}className="text-primary cursor-pointer font-size-14 mt-2 px-2"
key={props.data?.id}>
Short Text
</div>
<span>{ props.data?.caption}</span>
</>
}
It appears that the code sample in the question is repeated for each row, but there is only 1 state fullText (showMore in the CodeSandbox) that is common for all these rows. Hence they all open or close together.
If you want individual open/close feature for each row, then you need 1 such state per row. An easy solution could be to embed this state with the data of each row:
export default class App extends Component {
state = {
data: [
{
id: 1,
description: "aaaaa",
showMore: false // Initially closed
},
// etc.
]
};
render() {
return (
<>
{this.state.data.map((e) => (
<>
{ /* Read the showMore state of the row, instead of a shared state */
e.showMore === false ? (
<div onClick={() => this.setState({ data: changeShow(this.state.data, e.id, true) })}>
Show description{" "}
</div>
) : (
<>
<div onClick={() => this.setState({ data: changeShow(this.state.data, e.id, false) })}>
Show less{" "}
</div>
<span key={e.id}>{e.description} </span>
</>
)}
</>
))}
</>
);
}
}
/**
* Update the showMore property of the
* row with the given id.
*/
function changeShow(data, id, show) {
for (const row of data) {
if (row.id === id) { // Find the matching row id
row.showMore = show; // Update the property
}
}
return data;
}
there are two specific desired outputs you might want. you can either make it open only one row at a time. you can also make them individual row specific to open or close independently.
For making specific row behave on its own you can store a list of ids in state for the expanded view and show the relevant component or behaviour.
class RowSpecific extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedIds: [],
data: [
{ id: "1", otherInfo: {} },
{ id: "2", otherInfo: {} },
{ id: "3", otherInfo: {} },
],
};
this.handleExpandHide = this.handleExpandHide.bind(this);
}
handleExpandHide(expand, id) {
if (!id) return;
let sIds = [...this.state.selectedIds];
if (expand) sIds.push(id);
else sIds.remove((rId) => rId === id);
this.setState({
selectedIds: sIds,
});
}
render() {
let list = this.state.data.map((data) => {
let show = this.state.selectedIds.find((id) => data.id === id);
return show ? (
<ExpandedView
data={data}
onExpandHide={(e) => {
this.handleExpandHide(true, data.id);
}}
/>
) : (
<CollapseView
data={data}
onExpandHide={(e) => {
this.handleExpandHide(true, data.id);
}}
/>
);
});
return list;
}
}
For making it open only one at a time you can save the clicked id in state and then when showing it only the matched id in state should be shown as open . rest are closed.
class SingleRow extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedId: "2",
data: [
{ id: "1", otherInfo: {} },
{ id: "2", otherInfo: {} },
{ id: "3", otherInfo: {} },
],
};
}
render() {
let list = this.state.data.map((data) => {
let show = data.id === this.state.selectedId;
return show ? <ExpandedView data={data} /> : <CollapseView data={data} />;
});
return list;
}
}
I am trying to create an ObjectList component, which would contain a list of Children.
const MyList = ({childObjects}) => {
[objects, setObjects] = useState(childObjects)
...
return (
<div>
{childObjects.map((obj, idx) => (
<ListChild
obj={obj}
key={idx}
collapsed={false}
/>
))}
</div>
)
}
export default MyList
Each Child has a collapsed property, which toggles its visibility. I am trying to have a Collapse All button on a parent level which will toggle the collapsed property of all of its children. However, it must only change their prop once, without binding them all to the same state. I was thinking of having a list of refs, one for each child and to enumerate over it, but not sure if it is a sound idea from design perspective.
How can I reference a dynamic list of child components and manage their state?
Alternatively, is there a better approach to my problem?
I am new to react, probably there is a better way, but the code below does what you explained, I used only 1 state to control all the objects and another state to control if all are collapsed.
Index.jsx
import MyList from "./MyList";
function Index() {
const objList = [
{ data: "Obj 1", id: 1, collapsed: false },
{ data: "Obj 2", id: 2, collapsed: false },
{ data: "Obj 3", id: 3, collapsed: false },
{ data: "Obj 4", id: 4, collapsed: false },
{ data: "Obj 5", id: 5, collapsed: false },
{ data: "Obj 6", id: 6, collapsed: false },
];
return <MyList childObjects={objList}></MyList>;
}
export default Index;
MyList.jsx
import { useState } from "react";
import ListChild from "./ListChild";
const MyList = ({ childObjects }) => {
const [objects, setObjects] = useState(childObjects);
const [allCollapsed, setallCollapsed] = useState(false);
const handleCollapseAll = () => {
allCollapsed = !allCollapsed;
for (const obj of objects) {
obj.collapsed = allCollapsed;
}
setallCollapsed(allCollapsed);
setObjects([...objects]);
};
return (
<div>
<button onClick={handleCollapseAll}>Collapse All</button>
<br />
<br />
{objects.map((obj) => {
return (
<ListChild
obj={obj.data}
id={obj.id}
key={obj.id}
collapsed={obj.collapsed}
state={objects}
setState={setObjects}
/>
);
})}
</div>
);
};
export default MyList;
ListChild.jsx
function ListChild(props) {
const { obj, id, collapsed, state, setState } = props;
const handleCollapse = (id) => {
console.log("ID", id);
for (const obj of state) {
if (obj.id == id) {
obj.collapsed = !obj.collapsed;
}
}
setState([...state]);
};
return (
<div>
{obj} {collapsed ? "COLLAPSED!" : ""}
<button
onClick={() => {
handleCollapse(id);
}}
>
Collapse This
</button>
</div>
);
}
export default ListChild;
I'm updating two states where setProductsShow depends on setSelectedCategories. setSelectedCategories is updated first then setProductsShow is updated to render products.
There are plenty of solutions on React state updates and I've read other answers on the matter but I'm confused on the order of which the states update for my specific case.
products
[
{
"name": "Potato",
"category": "food"
},
{
"name": "iPhone",
"category": "tech"
}
]
menuItems
[
{
"name": "tech",
"menuSelect": false
},
{
"name": "food",
"menuSelect": false
}
]
const [productsShow, setProductsShow] = useState(products)
const [selectedCategories, setSelectedCategories] = useState(menuItems) // Default all items selected
// Event is triggered by a change in menu selection; code not shown
function handleChangeMenu(event) {
let { name, checked } = event.target
// Update categories based on menu selection
setSelectedCategories(category => (
category.map(item => item.name == name ? {
...item,
menuSelect: checked
} : item)
))
// If category is selected, then push onto an array
let flatSelected = []
setSelectedCategories(c => {
flatSelected = c.map(selected => {
if (selected.menuSelect) {
flatSelected.push(selected.name)
}
})
return c // Return the state since we're only reading
})
console.log("FLATSELECTED")
console.log(flatSelected)
// Render products which are marked as TRUE in array of menu items "flatSelected[]"
setProductsShow(() => {
console.log("INSIDE SETPRODUCTSSHOW")
console.log(products.filter(product => flatSelected.includes(product.category)))
console.log("FLATSELECTED")
console.log(flatSelected)
products.filter(product => flatSelected.includes(product.category))
})
}
The outcome I get is console.log(products.filter(product => flatSelected.includes(product.category))) prints out an empty array.
The problem is that you are setting state and try to access the state before the component has been rerenderd. The second time you call setSelectedCategories categories hasn't had time to get updated.
Set state at the end of the function and it should work:
import { useState } from "react";
const products = [
{
name: "Potato",
category: "food",
},
{
name: "iPhone",
category: "tech",
},
];
const menuItems = [
{
name: "tech",
menuSelect: false,
},
{
name: "food",
menuSelect: false,
},
];
function App() {
const [productsShow, setProductsShow] = useState(products);
const [selectedCategories, setSelectedCategories] = useState(menuItems);
function handleChangeMenu(event) {
//mock the event
let { name, checked } = { name: "tech", checked: true };
//updating catagory
const newSelectedCategories = menuItems.map((category) =>
category.name === name ? { ...category, menuSelect: checked } : category
);
//updating products
const newProductsShow = products.filter((product) =>
newSelectedCategories.some(
({ name, menuSelect }) => name === product.category && menuSelect
)
);
// Update state based on menu selection
setSelectedCategories(newSelectedCategories);
setProductsShow(newProductsShow);
}
return (
<div>
<button onClick={handleChangeMenu}> Toogle tech </button>
{productsShow.map((product) => (
<div> {product.name}</div>
))}
</div>
);
}
export default App;
I using Table from ant desing. I have question about how can i re-render table datasource when i update the state ? I add state to dataSource like :
<Table
rowKey="Id"
columns={columns}
dataSource={documents.Data}
className="w-full"
/>
And after i update my state like :
const UpdateDocument = (row) => {
let fakeList = { ...documents };
let item = fakeList.Data.find((x) => x.Id === row.Id);
let index = fakeList.Data.indexOf(item);
fakeList.Data[index] = row;
setDocuments(fakeList); //I update my state here.
};
But its not re-render table datasource. After i update my state shouldn't it re-render too ? Where do i make wrong ? Thanks for replies!
You are updating the same reference, you need to render a copy or your component will not render.
A working example based on your codesandbox:
import "./styles.css";
import React, { useState } from "react";
import { Button, Table } from "antd";
const fakeData = {
Data: [
{
Name: "Test 1",
isOpen: true
},
{
Name: "Test 2",
isOpen: true
},
{
Name: "Test 3",
isOpen: true
},
{
Name: "Test 4",
isOpen: true
}
]
};
export default function App() {
const [newData, setnewData] = useState(fakeData);
const changeState = () => {
const fakeItem = { Name: "Fake test", isOpen: true };
const newList = { Data: []};
const datas = fakeData.Data.map((data) =>
data.Name === "Test 3" ? fakeItem : data
);
newList.Data = datas;
setnewData(newList);
};
const columns = [
{
title: "Name",
dataIndex: "Name",
key: "name"
}
];
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<Table rowKey="Name" columns={columns} dataSource={newData.Data} />
<Button onClick={changeState}>Change State</Button>
</div>
);
}
I am trying to sort the table data based on the field selected from the dropdown. It should alternatively sort between ascending or descending whenever the same field is clicked. I am maintaining a sort object that decides which field is selected and what is the sort order. Then it is sent to lodash orderBy with the field and the order. It does not work
This is what I have tried. Can some one tell me what I am doing wrong. Help is really appreciated.
https://codesandbox.io/s/simple-react-class-component-1n3f9?file=/src/index.js:0-3000
import React from "react";
import ReactDOM from "react-dom";
import "semantic-ui-css/semantic.min.css";
import { Dropdown } from "semantic-ui-react";
import moment from "moment";
import orderby from "lodash.orderby";
const options = [
{ key: 1, text: "Name", value: "name", icon: "sort" },
{ key: 2, text: "Time", value: "time", icon: "sort" },
{ key: 3, text: "Type", value: "type", icon: "sort" }
];
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [],
original: [],
sortObject: { field: "", order: "" }
};
}
componentDidMount() {
let list = [
{
name: "namev1",
time: 1583295463213,
type: 14
},
{
name: "namea2",
time: 1582885423296,
type: 15
},
{
name: "namea3",
time: 1581295463213,
type: 16
}
];
this.setState({ list, original: list });
}
handleSearch = e => {
let searchInput = e.target.value;
let filteredData = this.state.original.filter(value => {
return (
value.name.toLowerCase().includes(searchInput.toLowerCase()) ||
value.type.toString().includes(searchInput.toString())
);
});
this.setState({ list: filteredData });
};
formSortObject = fieldName => {
let { sortObject } = this.state;
if (!sortObject.field || sortObject.field !== fieldName) {
Object.assign(sortObject, {
field: fieldName,
order: "asc"
});
return sortObject;
} else if (sortObject.field === fieldName) {
Object.assign(sortObject, {
...sortObject,
order: sortObject.order === "desc" ? "asc" : "desc"
});
return sortObject;
}
};
handleSort = (e, data) => {
let dropdDownValue = data.value;
let currentField = this.formSortObject(dropdDownValue);
let result = orderby(
this.state.list,
currentField.field,
currentField.order
);
this.setState({ list: result });
};
render() {
return (
<>
Search: <input type="text" onChange={this.handleSearch} />
<Dropdown text="Sort By" options={options} onChange={this.handleSort} />
<h1>List</h1>
<table>
<tbody>
{this.state.list.map((item, index) => (
<tr key={index}>
<td>
<p>{index + 1}</p>
</td>
<td>
<p>{item.name}</p>
</td>
<td>
<p>{moment().diff(item.time, "days")}</p>
</td>
<td>
<p>{item.type}</p>
</td>
</tr>
))}
</tbody>
</table>
</>
);
}
}
Your code isnt's working as you spected because you are calling the handleSort function only when the value of the select change (see the onChange of your <Dropdown />).
What you need is the function executed when an option is clicked.
I searched for the documentation of the library you are using and I came to the conclusion that what you need is this.
<Dropdown text='Sort By'>
<Dropdown.Menu>
{options.map(item=>
<Dropdown.Item text={item.text} value={item.value} icon={item.icon} key={item.key} onClick={this.handleSort}/>
)}
</Dropdown.Menu>
</Dropdown>
I tried it in your codesandbox and it works perfectly!
I hope it helps!