I have a problem selecting a single checkbox or multiple checkbox in a table in React. I'm using Material-UI. Please see my codesandbox here
CLICK HERE
I wanted to achieve something like this in the picture below:
<TableContainer className={classes.tableContainer}>
<Table>
<TableHead className={classes.tableHead}>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
checked={false}
inputProps={{ "aria-label": "select all desserts" }}
/>
</TableCell>
{head.map((el) => (
<TableCell key={el} align="left">
{el}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{body?.excluded_persons?.map((row, index) => (
<TableRow key={row.id}>
<TableCell padding="checkbox">
<Checkbox checked={true} />
</TableCell>
<TableCell align="left">{row.id}</TableCell>
<TableCell align="left">{row.name}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
Seems you are just missing local component state to track the checked status of each checkbox, including the checkbox in the table header.
Here is the implementation for the AddedPersons component since it's more interesting because it has more than one row of data.
Create state to hold the selected persons state. Only add the additional local state, no need to duplicate the passed body prop data (this is anti-pattern anyway) nor add any derived state, i.e. is indeterminate or is all selected (also anti-pattern).
const [allSelected, setAllSelected] = React.useState(false);
const [selected, setSelected] = React.useState({});
Create handlers to toggle the states.
const toggleAllSelected = () => setAllSelected((t) => !t);
const toggleSelected = (id) => () => {
setSelected((selected) => ({
...selected,
[id]: !selected[id]
}));
};
Use a useEffect hook to toggle all the selected users when the allSelected state is updated.
React.useEffect(() => {
body.persons?.added_persons &&
setSelected(
body.persons.added_persons.reduce(
(selected, { id }) => ({
...selected,
[id]: allSelected
}),
{}
)
);
}, [allSelected, body]);
Compute the selected person count to determine if all users are selected manually or if it is "indeterminate".
const selectedCount = Object.values(selected).filter(Boolean).length;
const isAllSelected = selectedCount === body?.persons?.added_persons?.length;
const isIndeterminate =
selectedCount && selectedCount !== body?.persons?.added_persons?.length;
Attach all the state and callback handlers.
return (
<>
<TableContainer className={classes.tableContainer}>
<Table>
<TableHead className={classes.tableHead}>
<TableRow>
<TableCell colSpan={4}>{selectedCount} selected</TableCell>
</TableRow>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
checked={allSelected || isAllSelected} // <-- all selected
onChange={toggleAllSelected} // <-- toggle state
indeterminate={isIndeterminate} // <-- some selected
inputProps={{ "aria-label": "select all desserts" }}
/>
</TableCell>
...
</TableRow>
</TableHead>
<TableBody>
{body?.persons?.added_persons?.map((row, index) => (
<TableRow key={row.id}>
<TableCell padding="checkbox">
<Checkbox
checked={selected[row.id] || allSelected} // <-- is selected
onChange={toggleSelected(row.id)} // <-- toggle state
/>
</TableCell>
<TableCell align="left">{row.id}</TableCell>
<TableCell align="left">{row.name}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</>
);
Update
Seems there was a bug in my first implementation that disallowed manually deselecting people while the select all checkbox was checked. The fix is to move the logic in the useEffect into the toggleAllSelected handler and use the onChange event to toggle all the correct states. Also to add a check to toggleSelected to deselect "select all" when any person checkboxes have been deselected.
const [allSelected, setAllSelected] = React.useState(false);
const [selected, setSelected] = React.useState({});
const toggleAllSelected = (e) => {
const { checked } = e.target;
setAllSelected(checked);
body?.persons?.added_persons &&
setSelected(
body.persons.added_persons.reduce(
(selected, { id }) => ({
...selected,
[id]: checked
}),
{}
)
);
};
const toggleSelected = (id) => (e) => {
if (!e.target.checked) {
setAllSelected(false);
}
setSelected((selected) => ({
...selected,
[id]: !selected[id]
}));
};
Note: Since both AddedPersons and ExcludedPersons components are basically the same component, i.e. it's a table with same headers and row rendering and selected state, you should refactor these into a single table component and just pass in the row data that is different. This would make your code more DRY.
I have updated your added person table as below,
please note that I am using the component state to update the table state,
const AddedPersons = ({ classes, head, body }) => {
const [addedPersons, setAddedPersons] = useState(
body?.persons?.added_persons.map((person) => ({
...person,
checked: false
}))
);
const [isAllSelected, setAllSelected] = useState(false);
const [isIndeterminate, setIndeterminate] = useState(false);
const onSelectAll = (event) => {
setAllSelected(event.target.checked);
setIndeterminate(false);
setAddedPersons(
addedPersons.map((person) => ({
...person,
checked: event.target.checked
}))
);
};
const onSelect = (event) => {
const index = addedPersons.findIndex(
(person) => person.id === event.target.name
);
// shallow clone
const updatedArray = [...addedPersons];
updatedArray[index].checked = event.target.checked;
setAddedPersons(updatedArray);
// change all select checkbox
if (updatedArray.every((person) => person.checked)) {
setAllSelected(true);
setIndeterminate(false);
} else if (updatedArray.every((person) => !person.checked)) {
setAllSelected(false);
setIndeterminate(false);
} else {
setIndeterminate(true);
}
};
const numSelected = addedPersons.reduce((acc, curr) => {
if (curr.checked) return acc + 1;
return acc;
}, 0);
return (
<>
<Toolbar>
{numSelected > 0 ? (
<Typography color="inherit" variant="subtitle1" component="div">
{numSelected} selected
</Typography>
) : (
<Typography variant="h6" id="tableTitle" component="div">
Added Persons
</Typography>
)}
</Toolbar>
<TableContainer className={classes.tableContainer}>
<Table>
<TableHead className={classes.tableHead}>
<TableRow>
<TableCell padding="checkbox">
<Checkbox
checked={isAllSelected}
inputProps={{ "aria-label": "select all desserts" }}
onChange={onSelectAll}
indeterminate={isIndeterminate}
/>
</TableCell>
{head.map((el) => (
<TableCell key={el} align="left">
{el}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{addedPersons?.map((row, index) => (
<TableRow key={row.id}>
<TableCell padding="checkbox">
<Checkbox
checked={row.checked}
onChange={onSelect}
name={row.id}
/>
</TableCell>
<TableCell align="left">{row.id}</TableCell>
<TableCell align="left">{row.name}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</>
);
};
export default AddedPersons;
Please refer to this for a working example: https://codesandbox.io/s/redux-react-forked-cuy51
Related
in my project I show a list where all the pokemons categories are.
When the user clicks on a certain category the list is updated.
My list is updating, but the problem is that my component is not re-rendering again with the new list items.
Here's my code I put into codesandbox
import React from "react";
import { types, pokemons } from "./data";
import Avatar from "./components/Avatar";
import List from "./components/List";
import "./styles.css";
const App = () => {
const [favorite, setFavorite] = React.useState("rock");
console.log(favorite);
const _data = [];
React.useMemo(
() =>
pokemons.map((pokemon, i) => {
if (pokemon.type.includes(favorite.toLowerCase())) {
_data.push(pokemon);
}
return _data;
}),
[_data, favorite]
);
const removeDup = [];
_data.reduce((acc, curr) => {
if (acc.indexOf(curr.name) === -1) {
acc.push(curr.name);
removeDup.push(curr);
}
return acc;
}, []);
return (
<div className="App">
<Avatar data={types} setFavorite={setFavorite} />
<List data={removeDup} />
</div>
);
};
export default App;
List
const List = ({ data }) => {
const [pokemonsState, setPokemonsState] = useState(data);
const [isAscSort, setIsAscSort] = useState(false);
const sortPokemon = () => {
if (isAscSort)
setPokemonsState(stableSort(data, getComparator("asc", "name")));
else setPokemonsState(stableSort(data, getComparator("desc", "name")));
setIsAscSort(!isAscSort);
};
return (
<TableContainer>
<Table sx={{ minWidth: 650 }}>
<TableHead>
<TableRow>
<TableCell>Pokémon</TableCell>
<TableCell name onClick={() => sortPokemon()} align="right">
Name
{!isAscSort ? <ArrowUpward /> : <ArrowDownward />}
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{pokemonsState.map((pokemon, idx) => (
<TableRow
key={idx}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
>
<TableCell component="th" scope="row" thumbnailImage>
<div className="thumb">
<img src={pokemon.thumbnailImage} alt="" />
</div>
</TableCell>
<TableCell align="left" component="th" scope="row" description>
{pokemon.name}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
Could you tell me what I'm doing wrong?
Thank you very much in advance!!!
Issues
You've at least a couple issues.
Using the mapped array index as the React key is generally an anti-pattern, especially if you are filtering, sorting, mutating the underlying array being mapped.
The List component doesn't update its pokemonsState state when the data prop updates.
Solution
Use a useEffect with a dependency on the data prop to update the local pokemonsState state. use the pokemon.id as the React key, assuming all pokemon have unique id properties.
const List = ({ data }) => {
const [pokemonsState, setPokemonsState] = useState(data);
const [isAscSort, setIsAscSort] = useState(false);
// Update local state when prop updates
useEffect(() => {
setPokemonsState(data);
}, [data]);
const sortPokemon = () => {
if (isAscSort)
setPokemonsState(stableSort(data, getComparator("asc", "name")));
else setPokemonsState(stableSort(data, getComparator("desc", "name")));
setIsAscSort(!isAscSort);
};
return (
<TableContainer>
<Table sx={{ minWidth: 650 }}>
...
<TableBody>
{pokemonsState.map((pokemon, idx) => (
<TableRow
key={pokemon.id} // <-- use unique React key
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
>
...
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
);
};
Your List component is trying to copy the data prop into its state. Copying props into state is usually a bad idea. If that prop changes, the List will ignore the change and continue using its state. Only once something changes the state (eg, clicking on the sort button) will you get back in sync.
I would recommend that you delete the state and instead compute the sorted list from the prop. This computation can be put inside of useMemo to improve performance by skipping calculating if nothing has changed:
const List = ({ data }) => {
const [isAscSort, setIsAscSort] = useState(false);
const sortedPokemons = useMemo(() => {
if (isAscSort) {
return stableSort(data, getComparator("asc", "name"))
} else {
return stableSort(data, getComparator("desc", "name"))
}
}, [data, isAscSort]);
const sortPokemon = () => {
setIsAscSort(!isAscSort);
};
// ...
<TableBody>
{sortedPokemons.map((pokemon, idx) => (
// ...
)}
</TableBody>
}
I don't know if the question captures what I had in my but I will explain below...
I fetched data from API and mapped into a collapsible table. Full details of the data should be embedded in EACH row such that onclick on each row, reveals the full details. Here's the code below
function Encounter() {
const [open2, setOpen2] = useState(false);
const [details, setDetails] = useState([]);
const getDetails = async () => {
try {
const fetch = await Axios.get(
"https://pshs3.herokuapp.com/all/encounter"
);
setDetails(fetch.data.data)
} catch (err) {
console.log(err);
}
};
useEffect(() => {
getDetails();
}, []);
return (
<Wrapper>
<Table stickyHeader aria-label="sticky table">
<TableHead>
<TableRow>
<TableCell/>
<TableCell >
Enrollment ID
</TableCell>
<TableCell >
Encounter
</TableCell>
<TableCell>
Facility Code
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{details.map((detail, idx) => {
return (
<>
<TableRow sx={{ "& > *": { borderBottom: "unset" } }}>
<TableCell>
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? (
<KeyboardArrowUpIcon />
) : (
<KeyboardArrowDownIcon />
)}
</IconButton>
</TableCell>
<TableCell key={idx}>
{detail.enrollment_id}
</TableCell>
<TableCell key={idx}>
{detail.encounter}
</TableCell>
<TableCell key={idx}>
{detail.faciity_code}
</TableCell>
</TableRow>
<TableRow>
<TableCell
style={{ paddingBottom: 0, paddingTop: 0 }}
colSpan={6}
>
<Collapse in={open} timeout="auto" unmountOnExit>
<Box sx={{ margin: 1 }}>
<Typography variant="h6" gutterBottom component="div">
Details
</Typography>
<Tooltip />
</Box>
</Collapse>
</TableCell>
</TableRow>
</>
);
})}
</TableBody>
</Table>
</Wrapper>
);
}
export default Encounter;
The problem I have is how to implement the open and setOpen state to individual row, also the Tooltip component(which is a the full table details from the API) to display full details of each row onclick which should correspond to the selected row in question.
Here are two solutions to the first problem.
1. Create a separate component for each <TableRow />
This component will have its own state and allows you to collapse/expand each row individually.
2. Use a dictionary for the open state
Since you have multiple (dynamic) rows, you can introduce a dictionary for the open state.
const [open, setOpen] = useState({});
For each row, you will use the open[idx] property to determine if the row should be "open"
<Collapse in={open[idx]} timeout="auto" unmountOnExit>
And in the <IconButton /> component, set the state based on the current row state.
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(current => ({ ...current, [idx]: !current[idx] }))}
>
Firstly add a variable in your details object:
const getDetails = async () => {
try {
const fetch = await Axios.get(
"https://pshs3.herokuapp.com/all/encounter"
);
const response = fetch.data.data;
response.map((elem) => elem.open = false)
setDetails(response)
} catch (err) {
console.log(err);
}
};
Then you can change the open variable for each element in details:
<IconButton
aria-label="expand row"
size="small"
onClick={() => detail.open = !detail.open)}
>
You might need to update the state, so change your onClick:
onClick={() => changeOpenStatus(idx))}
and the function:
const changeOpenStatus = (idx) => {
const newDetails = {...details}
newDetails[idx].open = !newDetails[idx].open;
setDetails(newDetails)
}
I have a problem with the management of the data, when i try to render some data from the pokemon api my table render multiple times the titles, i tried to move only the data to a different component but not luck.
How can i fix this?
API CAll
export const PokemonApi = () => {
const [poke, setPoke] = useState([]);
const data = () => {
axios.get('https://pokeapi.co/api/v2/pokemon?limit=10&offset=20').then(( response ) => {
setPoke(response.data.results);
console.log(response.data.results);
})
.catch( err => {
console.log(err);
})
}
useEffect(() => {
data()
}, []);
return (
<>
{
poke.map(( info, name ) => {
return <Lista key={ name } info={ info } />
})
}
</>
)
}
component where I try to render
export const Lista = (props) => {
const classes = useStyles();
return (
<div>
<Container maxWidth="md">
<TableContainer component={Paper}>
<Table className={ classes.table } size="small" aria-label="a dense table">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell align="right">URL</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow key={ props.info.name }>
<TableCell component="th" scope="row">
{ props.info.name }
</TableCell>
<TableCell align="right">{ props.info.url }</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Container>
</div>
)
}
This is the page where i render the table
import React from 'react'
import { PokemonApi } from '../api/PokemonApi'
export const Pokes = () => {
return (
<>
<PokemonApi />
</>
)
}
And here is the table.
I hope anyone can help me!
As your code is written, you are not rendering one table with a row for each line. You are creating one Lista par record, you have as many tables as pokemon.
What you are looking to achieve is more like :
function PokemonRow(props) {
return (
<TableRow key={ props.info.name }>
<TableCell component="th" scope="row">
{ props.info.name }
</TableCell>
<TableCell align="right">{ props.info.url }</TableCell>
</TableRow>
)
}
export const PokemonTable() {
const classes = useStyles();
const [poke, setPoke] = useState([]);
const data = () => {
axios.get('https://pokeapi.co/api/v2/pokemon?limit=10&offset=20').then(( response ) => {
setPoke(response.data.results);
console.log(response.data.results);
})
.catch( err => {
console.log(err);
})
}
useEffect(() => {
data()
}, []);
return (
<div>
<Container maxWidth="md">
<TableContainer component={Paper}>
<Table className={ classes.table } size="small" aria-label="a dense table">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell align="right">URL</TableCell>
</TableRow>
</TableHead>
<TableBody>
{poke.map(infos => <PokemonRow info={infos}/>)}
</TableBody>
</Table>
</TableContainer>
</Container>
</div>
)
}
I'm using Reactjs and MaterialIU v.3 (remember v3 not v4), I'm trying to implement a Table using [array].map to building it, and rendering a Select inside TableCell, expecting to define a specific percentage by separate for each service (lunch, snack, transportation), but when I've changed anyone, the selected percentage changes all the selector. Have anyone an idea to achieve the approach.
Codes & image:
<Table className={classes.table}>
<TableHead>
<TableRow>
<TableCell>Code</TableCell>
<TableCell>Name</TableCell>
<TableCell>Value</TableCell>
<TableCell>Perc.</TableCell>
</TableRow>
</TableHead>
<TableBody>
{this.state.fees_details.map((item, key) => {
return (
<TableRow key={key}>
<TableCell>
{item.ItemCode}
</TableCell>
<TableCell>
{item.Dscription}
</TableCell>
<TableCell>
{item.Price}
</TableCell>
<TableCell>
<Select
value={this.state.percentControl}
onChange={this.selectItemOnChange}
inputProps={{
name: item.ItemCode,
id: item.ItemCode
}}>
<MenuItem value={0}>0%</MenuItem>
<MenuItem value={10}>10%</MenuItem>
<MenuItem value={20}>20%</MenuItem>
<MenuItem value={30}>30%</MenuItem>
<MenuItem value={40}>40%</MenuItem>
<MenuItem value={50}>50%</MenuItem>
<MenuItem value={60}>60%</MenuItem>
<MenuItem value={70}>70%</MenuItem>
<MenuItem value={80}>80%</MenuItem>
<MenuItem value={90}>90%</MenuItem>
<MenuItem value={100}>100%</MenuItem>
</Select>
</TableCell>
</TableRow>
) })}
</TableBody>
And my function selectItemOnChange definition is:
selectItemOnChange(e) {
console.log(e)
if (e.target.name === '0024') {
console.log(e.target.value)
this.setState({
percentControl: e.target.value
})
}
if (e.target.name === '0025') {
console.log(e.target.value)
this.setState({
percentControl: e.target.value
})
}
if (e.target.name === '0027') {
console.log(e.target.value)
this.setState({
percentControl: e.target.value
})
}
}
Image:
Thank you in advanced.
You can't make it happen as well as using one single value of status.
Make it an Object or an Array.
You can pass the identity index as params in the handler function.
Below is a minimal reproducible example which you can refer
const list = [...Array(10).keys()].map(x => ({ id: x, ItemCode: x }));
const App = () => {
const [selected, setSelected] = React.useState(list);
const onChangeHandler = id => e => {
setSelected([
...selected.filter(x => x.id !== id),
{ id: id, ItemCode: e.target.value }
]);
};
return (
<div className="App">
{list.map(item => (
<select
key={item.id}
value={selected.find(x => x.id === item.id).ItemCode}
onChange={onChangeHandler(item.id)}
>
<option value={0}>0%</option>
<option value={1}>10%</option>
<option value={2}>20%</option>
<option value={3}>30%</option>
</select>
))}
<br />
</div>
);
};
ReactDOM.render(<App />, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.12.0/umd/react-dom.production.min.js"></script>
I have a table with players that is loaded in from an API call and rendered using a map function. The last column of the table contains a delete button. When clicking the delete button, I would like to disable all the other delete buttons until the delete call is completed.
Here is the function that performs the API call to get the players.
loadPlayers() {
const { cookies } = this.props;
const AuthStr = 'Bearer '.concat(this.state.access_token);
axios.get(
configVars.apiUrl + 'team/get-team',
{ headers: { Authorization: AuthStr } }
).then(response => {
var teamArray = response.data;
//Information gotten
this.setState({
teamArray: teamArray,
});
}).catch((error) => {
//Error, remove the access token from cookies and redirect home.
cookies.remove('access_token');
this.props.history.push("/");
});
}
}
Mapping and rendering is done like this:
<Grid item xs={12}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell align="right">Tier</TableCell>
<TableCell align="right">Value</TableCell>
<TableCell align="right">Delete</TableCell>
</TableRow>
</TableHead>
{this.state.teamArray.map((player, i) => {
return <TableBody key={i}>
<TableRow key={i}>
<TableCell align="left">{player.full_name}</TableCell>
<TableCell align="right">{player.tier}</TableCell>
<TableCell align="right">{player.value}</TableCell>
<TableCell align="right">
<IconButton disabled={this.state.deleteDisable}
onClick={() => {this.handleDelete(player.id)}} >
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
</TableBody>;
})}
</Table>
</Grid>
Inside the handleDelete function I start by setting deleteDisabledin the state to true. However, this has no effect since disabled is set to false once the table is loaded and never changed after.
How do I make sure this.state.deleteDisable is passed to the button as a variable instead of assigned once?
You should store the players into the state, then in the render method you can display the table
function loadPlayer() {
const { cookies } = this.props;
const AuthStr = 'Bearer '.concat(this.state.access_token);
axios.get(
configVars.apiUrl + 'team/get-team',
{ headers: { Authorization: AuthStr } }
)
.then(response => this.setState({players: response.data})})
.catch((error) => {
// Error ....
});
}
render() {
return (
...
{
this.state.players((player, i) => (
<TableBody key={i}>
<TableRow>
<TableCell align="left">{player.full_name}</TableCell>
<TableCell align="right">{player.tier}</TableCell>
<TableCell align="right">{player.value}</TableCell>
<TableCell align="right">
<IconButton disabled={this.state.deleteDisabled}
onClick={() => {this.handleDelete(player.id)}} >
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
</TableBody>
)}
...
);
}