sort table based on a certain column - reactjs

I'm new to both react and Tailwind CSS. I've created a table. Table columns are related (each name in the 1st column has a related mobile number in the 2nd column). I want to add an option on each column of this table, so that when I click on the header of a column, the table rows become sorted (alphabetically or numerically) according to that column. Here is the code:
import React, { useState, useEffect } from 'react'
import { getUsers } from '../../services/userService'
const Table = () => {
const [users, setUsers] = useState([]);
const [currentUsers, setCurrentUsers] = useState([]);
const [search, setSearch] = useState('');
const [isSorted, setIsSorted] = useState(false);
const [sortedUsers, setSortedUsers] = useState([]);
useEffect(async () => {
try {
const response = await getUsers(search);
setUsers(response.data.users);
setPageCount(Math.ceil(response.data.users.length / pageItemCount))
setCurrentUsers(response.data.users.slice(0, pageItemCount))
} catch (error) { }
}, [search]);
const handleChange = (event, value) => {
changePage(value);
}
const sortFn = (userA, userB) => {
// sort logic here, it can be whatever is needed
// sorting alphabetically by `first_name` in this case
return userA.first_name.localeCompare(userB.first_name)
}
const toggleSort = () => {
setIsSorted(!isSorted)
}
// when `currentUsers` changes we want to reset our table
// in order to keep it in sync with actual values
// we're also sorting if we were already sorting
useEffect(() => {
if (isSorted) {
setSortedUsers(currentUsers.slice().sort(sortFn))
} else {
setSortedUsers(currentUsers)
}
}, [isSorted, currentUsers])
return (
<div dir='rtl' className='bg-background mt-10 px-5 rd1200:px-30 overflow-auto'>
<table className='w-full border-separate rounded-md'>
<thead>
<tr className='bg-text-secondary text-white shadow-sm text-center'>
<th className='p-2' onClick={(e) => toggleSort()}>name</th>
<th className='p-2' onClick={(e) => toggleSort()}>mobile</th>
</tr>
</thead>
<tbody>
{sortedUsers.map((item, index) =>
<tr key={item.id} className={index % 2 === 0 ? 'bg-white shadow-sm text-center' : 'bg-text bg-opacity-5 shadow-sm text-center'}>
<td className='text-text text-sm p-2'>{item.first_name}</td>
<td className='text-text text-sm p-2'>{item.mobile}</td>
</tr>
)}
</tbody>
</table>
</div>
)
}
export default Table
The code is working fine. The only problem is that the table is only sorted based on the first name regardless of which column header I click on (So when I click on the mobile column header, the table is still sorted based on the first_name). How can I change it, so that the table content become sorted according to the clicked column header?

You should have some state which holds the key which the data is currently sorted by. Whenever a header is clicked update that state to whatever column was clicked.
On every re-render you can then use the key to access the values to sort by.
Please note: This sort() function will just compare strings and numbers and will only compare if the datatype matches.
const users = [
{ fname: "Thomas", lname: "Fox", age: 51 },
{ fname: "John", lname: "Mayor", age: 18 },
{ fname: "Ronny", lname: "Bush", age: 32 },
{ fname: "Aaron", lname: "Schulz", age: 73 },
];
const cols = [
{ key: "fname", text: "First name" },
{ key: "lname", text: "Last name" },
{ key: "age", text: "Age" },
];
const Table = () => {
const [data, setData] = React.useState(users);
const [columns, setColumns] = React.useState(cols);
const [sortedBy, setSortedBy] = React.useState(columns[0].key);
console.log(`Sorting by column ${sortedBy}`);
const sorted = data.sort((a, b) => {
const aVal = a[sortedBy];
const bVal = b[sortedBy];
if (typeof aVal === "number" && typeof bVal === "number") return aVal - bVal;
else if (typeof aVal === "string" && typeof bVal === "string") return aVal.localeCompare(bVal);
return 1;
});
return (
<table>
<thead>
<tr className="">
{columns.map((col) => (
<th onClick={() => setSortedBy(col.key)}>{col.text}</th>
))}
</tr>
</thead>
<tbody>
{sorted.map((item) => (
<tr>
<td>{item.fname}</td>
<td>{item.lname}</td>
<td>{item.age}</td>
</tr>
))}
</tbody>
</table>
);
};
ReactDOM.render(<Table />, document.getElementById("root"));
th {
border: solid thin;
padding: 0.5rem;
}
table {
border-collapse: collapse;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Instead of just the key you could additionally store whether to store in ascending or descending order for example when we click on a column again we toggle the descending order.
This is just a basic example which should give you some idea on how to implement such functionality.

Related

Change sorting order from ascending to descending

I'm new to ReactJS. I have a table with 2 columns. I want to sort the table based on the column header that is clicked on. Here is the code:
import React, { useState, useEffect } from 'react'
import { getUsers } from '../../services/userService'
const Table = () => {
const [users, setUsers] = useState([]);
const [currentUsers, setCurrentUsers] = useState([]);
const [isSorted, setIsSorted] = useState(false);
const [valueHeader, setValueHeader] = useState({title: "",body: ""}); //Value header state
const [sortedUsers, setSortedUsers] = useState([]);
useEffect(async () => {
try {
const response = await getUsers(search);
setUsers(response.data.users);
} catch (error) { }
}, [search]);
const sortFn = (userA, userB) => {
// sort logic here, it can be whatever is needed
// sorting alphabetically by `first_name` in this case
return userA[valueHeader.body].localeCompare(userB[valueHeader.body]) //<== Use value of column header
}
useEffect(() => {
if (isSorted) {
setSortedUsers(currentUsers.slice().sort(sortFn))
} else {
setSortedUsers(currentUsers)
}
}, [isSorted, currentUsers, valueHeader]) //<== add valueHeader to dependency
const toggleSort = (target) => {
setIsSorted(!isSorted)
setValueHeader({
title: target,
body: target == "name" ? "first_name" : "mobile_number"
}) //<=== set state of value header
}
return (
<div dir='rtl' className='bg-background mt-10 px-5 rd1200:px-30 overflow-auto'>
<table className='w-full border-separate rounded-md'>
<thead>
<tr className='bg-text-secondary text-white shadow-sm text-center'>
<th className='p-2' onClick={()=>toggleSort("name")}>name</th>
<th className='p-2' onClick={()=>toggleSort("mobile")}>mobile</th>
</tr>
</thead>
<tbody>
{sortedUsers.map((item, index) =>
<tr key={item.id} className={index % 2 === 0 ? 'bg-white shadow-sm text-center' : 'bg-text bg-opacity-5 shadow-sm text-center'}>
<td className='text-text text-sm p-2'>{item.first_name}</td>
<td className='text-text text-sm p-2'>{item.mobile_number}</td>
</tr>
)}
</tbody>
</table>
</div>
)
}
export default Table
My code is working fine. When I click on a column header, the table is sorted in the ascending order. However, when I re-clicked on the column header, the table becomes unsorted. A Cycle like this:
Unsorted === click ====> ascending ...
Is it possible to sort the table in descending order when I re-click on the column header? I mean a cycle like this:
Unsorted === click ====> ascending === click ===> descending ...

How to toggle in a map function in React?

I try to load user history and show it inside a table.
Inside this table should be a button with "show more"
This button is a dropdown toggle button.
Everything working so far but when i click the button that is inside a map function all dropdowns open at the same time.
I know i've to use an index in the map function but I've no clue how i can use this index for my dropdown state.
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { useSelector } from 'react-redux';
function OrderHistory() {
const [history, setHistory] = useState([]);
const [dropdown, setDropdown] = useState(false);
const auth = useSelector((state) => state.auth);
const token = useSelector((state) => state.token);
const { user } = auth;
useEffect(() => {
const getHistory = async () => {
const res = await axios.get('/user/history', {
headers: { Authorization: token },
userid: user._id,
});
setHistory(res.data);
};
getHistory();
}, [token, setHistory]);
console.log(history);
const clickHandler = (index) => (event) => {
//event.preventDefault();
setDropdown(!dropdown);
console.log('clicked');
};
return (
<div className='history-page h-screen'>
<h2>History</h2>
<h4>You have {history.length} ordered</h4>
<table>
<thead>
<tr>
<th>Bestellnummer</th>
<th>Datum</th>
<th>Summe</th>
<th>Details</th>
</tr>
</thead>
<tbody>
{history.map((items, index) => (
<tr key={items._id}>
<td>
{items._id}
</td>
<td>{new Date(items.createdAt).toLocaleDateString()}</td>
<td>
{(
Math.round(
items.cartItems.reduce((x, item) => x + item.price, 0) * 100
) / 100
).toFixed(2)}
€
</td>
<td>
<button
class='bg-yellow-500 hover:bg-yellow-400 text-white font-bold py-2 px-4 rounded'
key={index}
onClick={clickHandler(index)}
>
Show more
</button>
<div className={dropdown ? '' : 'hidden'}>
<div>{items.firstName}</div>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
export default OrderHistory;
Initialize the dropdown state with null.
const [dropdown, setDropdown] = useState(null);
In clickHandler store, i just store the index.
const clickHandler = (index) => {
setDropdown((prev) => {
return prev === index ? null : index;
});
console.log('clicked', index);
};
Now in map function, check where you apply the condition to show the dropdown. I check if the dropdown is equal to that index, then that dropdown will visible otherwise add hidden class.
You can also use _id to show/hide dropdown instead of index
<td>
<button
class='bg-yellow-500 hover:bg-yellow-400 text-white font-bold py-2 px-4 rounded'
key={index}
onClick={() => clickHandler(index)}
>
Show more
</button>
<div
className={dropdown === index ? '' : 'hidden'}
// style={{ display: dropdown === index ? 'block' : 'none' }}
>
<div>{items.firstName}</div>
</div>
</td>

Django Rest Framework with React order not working

I have a list in my application which is created from two related tables(products and stocks). I want to order items by date which is in the Stocks table. My API returns well ordered list but in my app the list is not ordered. When I try to order by any of Products fields it works fine - also in react.
My view:
class StocksView(viewsets.ModelViewSet):
serializer_class = StocksSerializer
ordering_fields = ['date']
filter_backends = (filters.OrderingFilter,)
def get_queryset(self):
queryset = Stocks.objects.all().order_by('date')
return queryset
def perform_create(self, serializer):
serializer.save(user=self.request.user)
Method from React:
const fetchStocks = async () => {
try {
const response = await axiosInstance.get('/stocks/?ordering=date')
if (response) {
setStocksInfo(response.data)
console.log(response.data)
}
} catch (error) {
throw error;
}
}
And returned list from two merged tables:
{productsInfo.map(products => {
const product = stocksInfo.filter(e => e.product_id === products.product_id);
return (
product.map((stock, i) => (
<tr key={i} className="table-data">
<td className="col-3"> {products.name}</td>
<td className="col-3">{stock.date}</td>
<td className="col-1">{stock.count}</td>
</tr>
))
)
})}
Since you want to order based on the Stocks table, you should do the map from that array first.
You can make an implementation like this one, where you filter the stocks so it only has the ones matching from the productsInfo, and then mapping it.
{stocksInfo
.filter(e => productsInfo.some(p => p.product_id === e.product_id))
.map((stock, i) => {
const products = productsInfo.find(e => e.product_id === stock.product_id);
return (
<tr key={i} className="table-data">
<td className="col-3"> {products.name}</td>
<td className="col-3">{stock.date}</td>
<td className="col-1">{stock.count}</td>
</tr>
);
})}
But, since that is inefficient, I would recommend combining both arrays and then mapping them. You can combine them with this code:
const combination = stocksInfo
.map(item =>
Object.assign(
item,
productsInfo.find(({product_id}) => product_id === item.product_id)
)
)
.filter(item => item.name);
I also added a working example here:
const stocksInfo = [
{date: "date1", count: 1, product_id: 123},
{date: "date2", count: 2, product_id: 321},
{date: "date3", count: 3, product_id: 331},
{date: "date4", count: 4, product_id: 341},
];
const productsInfo = [
{name: "test4", product_id: 341},
{name: "test2", product_id: 321},
];
const combination = stocksInfo
.map(item =>
Object.assign(
item,
productsInfo.find(({product_id}) => product_id === item.product_id)
)
)
.filter(item => item.name);
const Example = ({title}) => {
return (
<div>
previous way
<table>
{productsInfo.map(products => {
const product = stocksInfo.filter(e => e.product_id === products.product_id);
return product.map((stock, i) => (
<tr key={i} className="table-data">
<td className="col-3"> {products.name}</td>
<td className="col-3">{stock.date}</td>
<td className="col-1">{stock.count}</td>
</tr>
));
})}
</table>
new way
<table>
{combination.map((stock, i) => {
return (
<tr key={i} className="table-data">
<td className="col-3"> {stock.name}</td>
<td className="col-3">{stock.date}</td>
<td className="col-1">{stock.count}</td>
</tr>
);
})}
</table>
</div>
);
};
// Render it
ReactDOM.render(
<Example title="Example using Hooks:" />,
document.getElementById("react")
);
table, td {
border: 1px solid black;
padding: 10px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="react"></div>

How to delete item seleted in table product

I am trying to delete a product, but it's doesn't show success. I do not know how to get the id of that product to delete
My button onClick = {handleDelete} is import from component in other folder. I try to create handleDelete function, but I missing something in this case.
This is my code for that section
import React, { useState, useEffect } from "react";
import { Container, Row, Col, Table } from "react-bootstrap";
import Loading from "../../components/Loading";
import Button from "../../components/Button/index"
import firebaseApp from "../../api/config";
const ProductTableList = ({
products,
loading,
fetchProductRequest
}) => {
useEffect(() => {
fetchProductRequest();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const firebaseDb = firebaseApp.database();
const [currentId, setCurrentId] = useState("");
if (loading) {
return (
<Container>
<Row>
<Col>
<Loading />
</Col>
</Row>
</Container>
);
}
const handleDelete = (id) => {
const productId = firebaseDb.ref().push().key;
if (window.confirm("Are you sure to delete this record?")) {
firebaseDb
.ref("products")
.child(`products/${productId}`)
.remove((err) => {
if (err) console.log(err);
else setCurrentId("");
});
}
}
const handleUpdate = (event) => {
//TODO
}
return (
<Table striped bordered hover className="product-table">
<thead>
<tr>
<th>No.</th>
<th className="image">Image</th>
<th>Name</th>
<th>Category</th>
<th>Price</th>
<th>Description</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{!!products && products.length > 0 ? (
products.map((product, index) => {
return (
<tr key={index}>
<td>{index}</td>
<td>{product.image}</td>
<td>{product.name}</td>
<td>{product.category}</td>
<td>{product.price}</td>
<td>{product.description}</td>
<td>
<Button onClick={handleDelete} btnText="Delete" />
<Button onClick={handleUpdate} btnText="Update" />
</td>
</tr>
);
})
) :
(
<tr><td className="center-title">Product list is empty!</td></tr>
)}
</tbody>
</Table>
)
}
export default ProductTableList;
Can anyone help me? How do I delete the product that I have selected
Can anyone explain or support for me why? Thank you so much
I made a example, you need to add your function on button click and use your item id to be removed.
import React, { useState, useEffect } from "react";
import { Table } from "react-bootstrap";
const ProductTableList = () => {
const [currentId, setCurrentId] = useState("");
const [products, setProducts] = useState([{
image: 'image',
name: '01',
category: '01',
price: '01',
description: '01'
},
{
image: 'image',
name: '02',
category: '02',
price: '02',
description: '02'
},
{
image: 'image',
name: '03',
category: '03',
price: '03',
description: '03'
}])
const handleDelete = (id) => {
const removeItem = products.filter((item) => item !== products[id])
setProducts(removeItem)
}
return (
<Table striped bordered hover className="product-table">
<thead>
<tr>
<th>No.</th>
<th className="image">Image</th>
<th>Name</th>
<th>Category</th>
<th>Price</th>
<th>Description</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{!!products && products.length > 0 ? (
products.map((product, index) => {
return (
<tr key={index}>
<td>{index}</td>
<td>{product.image}</td>
<td>{product.name}</td>
<td>{product.category}</td>
<td>{product.price}</td>
<td>{product.description}</td>
<td>
<button onClick={() => handleDelete(index)}>Delete</button>
</td>
</tr>
);
})
) :
(
<tr><td className="center-title">Product list is empty!</td></tr>
)}
</tbody>
</Table>
)
}
export default ProductTableList;
Also, avoid index as element key
{ items.map((item, index) => (<li key={index}>{item}</li>)) }
When a list item was added or removed, and the key kept the same, the React assumed that the DOM element had not changed, and the app could not render.
An alternative to cases that the list doesn't have a unique ID is to generate one using shortID.
https://www.npmjs.com/package/shortid

How to display button opposite of level in React?

How to display button opposite of its status. Suppose its showing open then the button should appear Withdraw and if the status is appearing Withdraw button should appear Open in Reactjs.
I have a Json object which carries the necessary information.
Here is code is what I have had tried..
const initial = {
description: [
{
name: 'Alex',
level: 'open',
},
{
name: 'Sycus',
level: 'open',
},
{
name: 'Rasku',
level: 'Withdraw',
}
]
}
export default function App() {
const [state, setState] = React.useState(initial)
const [show , setshow] = React.useState(true)
const handleClick = () => {
return
}
const butFunction = () => {
return state.description.map(sub => {
const { level } = sub
return (
if(level === 'open'){
<button>Withdraw</button>
}
else{
<button>Open</buton>
}
)
})
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<table>
<tbody>
{
state.description.map(desc => (
<tr>
<td>
{desc.name}
</td>
<td>
{desc.level}
</td>
<td>
{ show && butFunction()}
</td>
</tr>
))
}
</tbody>
</table>
</div>
</div>
);
}
Instead of rendering two different buttons for two conditions you can use ternary operator to conditionally change the text of single button like this:
return(
<button>{level===‘open’?’withdraw’:’open’}</button>
)
I don't think you need the map inside the butFunction. That would print 3 buttons for every entry. Instead, pass the function a parameter telling it which level it is.
Example below:
export default function App() {
const [state, setState] = React.useState(initial)
const [show , setshow] = React.useState(true)
// Use the level passed in
const butFunction = (level) => {
if(level === 'open'){
return <button>Withdraw</button>;
} else {
return <button>Open</button>;
}
}
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<div>
<table>
<tbody>
{state.description.map(desc => (
<tr>
<td>
{desc.name}
</td>
<td>
{desc.level}
</td>
<td>
{show && butFunction(desc.level)} // <- Pass it here
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
);
}
Not it will only return one button per entry.

Resources