How to set checked/unchecked functionality on Html table in Reactjs - javascript

I have an array of Json objects('filteredUsers' in below code) and I'm mapping them to a html table. Json object would look like {id: xyz, name: Dubov}
The below code would display a html table with a single column or basically a list. Each row will have name of user and a grey checkbox(unchecked) next to it initially. I want to select users in the table and when I select or click on any item in table, the checkmark has to turn green(checked).
<table className="table table-sm">
{this.state.filteredUsers && (
<tbody>
{this.state.filteredUsers.map((user) => (
<tr key={user.id}>
<td onClick={() => this.selectUser(user)}>
<span>{user.name}</span> //Name
<div className={user.selected? "checked-icon": "unchecked-icon"}> //Checkmark icon
<span class="checkmark"> </span>
</div>
</td>
</tr>
))}
</tbody>
)}
</table>
I tried setting a 'selected' key to each object. Initially object doesn't have 'selected' key so it will be false(all unchecked). I set onClick method for 'td' row which sets 'selected' key to object and sets it to true. Below function is called onClick of td or table item.
selectUser = (user) => {
user.selected = !user.selected;
};
Now the issue is this will only work if I re-render the page after every onClick of 'td' or table item. And I'm forced to do an empty setState or this.forcedUpdate() in selectUser method to trigger a re-render. I read in multiple answers that a forced re-render is bad.
Any suggestions or help would be highly appreciated. Even a complete change of logic is also fine. My end goal is if I select an item, the grey checkmark has to turn green(checked) and if I click on it again it should turn grey(unchecked). Similarly for all items. Leave the CSS part to me, but help me with the logic. Thanks.

How about something like this:
const Users = () => {
const [users, setUsers] = useState([])
useEffect(() => {
// Fetch users from the API when the component mounts
api.getUsers().then((users) => {
// Add a `selected` field to each user and store them in state
setUsers(users.map((user) => ({ ...user, selected: true })))
})
}, [])
const toggleUserSelected = (id) => {
setUsers((oldUsers) =>
oldUsers.map((user) =>
user.id === id ? { ...user, selected: !user.selected } : user
)
)
}
return (
<ul>
{users.map((user) => (
<li key={user.id} onClick={() => toggleUserSelected(user.id)}>
<span>{user.name}</span>
<div className={user.selected ? "checked-icon" : "unchecked-icon"}>
<span class="checkmark" />
</div>
</li>
))}
</ul>
)
}
I've used hooks for this but the principles are the same.

This looks to be a state issue. When updating data in your react component, you'll need to make sure it's happening in one of two ways:
The data is updated by a component higher up in the tree and then is passed to this component via props.
this will cause your component to re-render with the new props and data, updating the "checked" property in your HTML.
In your case, it looks like you're using this second way:
The data is stored in component state. Then, when you need to update the data, you'd do something like the below.
const targetUser = this.state.filteredUsers.find(user => user.id === targetId)
const updatedUser = { ...targetUser, selected: !targetUser.selected }
this.setState({ filteredUsers: [ ...this.state.filteredUsers, updatedUser ] })
Updating your state in this way will trigger an update to your component. Directly modifying the state object without using setState does not trigger the update.
Please keep in mind that, when updating objects in component state, you'll need to pass a new, full object to setState in order to trigger the update. Something like this will not work: this.setState({ filteredUsers[1].selected: false });
Relevant documentation

Related

Checkbox state gets empty after page change

I have an array of objects that looks like this:
const columns = [
{
key: "Source_campname",
title: "TS Camp Name",
customElement: function (row) {
return (
<FormControlLabel
control={
<Checkbox
checked={checkbox[row.id]}
key={row.id}
onChange={() =>
handleChange(row.Source_campname, row.id, checkbox)
}
name={row.id}
/>
}
label={[row.Source_campname]}
/>
);
}
},
{
key: "Tracker_campname",
title: "TR Camp Name"
}
];
You can see a "handleChange" function above, this is used to check/uncheck the component
The handleChange function looks like this:
const handleChange = (name, campid) => {
setCheckBox({ ...checkbox, [campid]: !checkbox[campid] });
};
You can also see a "customElement" function above. This function is rendered in another React component named ThanosTable. I will just write down part of the code where the rendering of customElement happens below.
return (
<> columnArray[0].customElement(row) </>
);
In the end you get 10 checkboxes, and you have a few pages that can be changed using pagination.
Do check my codesandbox link here for a working example:
https://codesandbox.io/s/magical-germain-8tclq
Now I have two problems:
Problem 1) If I select a few checkboxes, then go to second page and return, the checkbox state is empty and the original checkboxes are unselected. No idea why that is happening. How do I prevent that?
Problem 2) The value of checkbox state is always an empty object ({}) inside customElement function. You can see this by checking console.log(checkbox) inside customElement function (Check Line 76 in codesandbox). I thought it should be an array with selected checkbox items.
The useEffect hook embodies all the lifecycle events of a component. Therefore if you try to set checkbox in useEffect it'll infinitely update the component because updating state calls useEffect. This is probably why you see your state constantly being reset.
Instead, initialize your state with the rows before rendering.
const rows = [
...
];
let checkboxObj = {};
// if (rows) {
rows.forEach((e) => {
checkboxObj[e.id] = false;
});
const [checkbox, setCheckBox] = useState(checkboxObj);

React rerender component using hooks when property in an array of object changes

So I am in a situation where I have to change a particular property from an array of objects. When the property changes I want to rerender the component. Now, this works fine without any issues when use the setPropertyName of useState. But now I am just changing one property of the object instead of the entire object.
Here is the code that Im working on:
const [movieList, setMovieList] = useState([]);
Calling the setMovieList and passing an array will obviously cause a rerender.
Consider the following contents of movieList:
movieList = [
{
'name': 'Mulholland Dr.'
'year':2001,
'watched' : true,
'rating':0
},
{
'name': 'Zodiac'
'year':2007,
'watched' : false,
'rating':0
},
{
'name': 'Twin Peaks'
'year':2017,
'watched' : true,
'rating': 0
}]
Then I have a function which renders the list:
function showMovieList () {
return movieList.map((movie) => {
return (
<List.Item key={movie.imdbID}>
<div className="watchedCheckBoxContainer">
<input type="checkbox" onChange={(event) => movie.watched = event.target.checked} id={`cb1${movie.imdbID}`}/>
<label htmlFor={`cb1${movie.imdbID}`}><Image size='tiny' src={movie.Poster} /></label>
</div>
{/* <Image size='tiny' src={movie.Poster} /> */}
<List.Content>{movie.Title}</List.Content>
{movie.watched ? <Rating maxRating={5} onRate={(event, {rating}) => movie.userRating=rating}/> : null}
</List.Item>
)
});
}
As you can see , when the checkbox is clicked it changes the value of the watched property. A few lines later I'm checking if movie.watched == true then show the <Rating> component. But in this case, I'm not using setMoviesList to update the moviesList and hence the <Rating> component is not visible.
How can I use setMoviesList to update watched property of the particular movie whose checkbox I click on?
Okay.. I solved it by the following way:
function onMovieWatched (watched, index) {
const tempMoviesList = [...movieList];
tempMoviesList[index].watched = watched;
setMovieList(tempMoviesList);
}
<input type="checkbox" onChange={(event) => onMovieWatched(event.target.checked, idx)} id={`cb1${movie.imdbID}`}/>
The idx is the index that I am using from the map method.
Initially I was afraid that I might have to loop over the entire array and get the object that matches the imdbID and then update its property.
Luckily I have the index while mapping over it, so I just used that to directly retrieve the object.
Dont know why I didnt think of this solution before posting.

Dispatch Toggle in Redux without saving it in State?

How I do it right now
I have a list of items. Right now, when the user presses button X, shouldShowItem is toggled. shouldShowItem ultimately lies in redux and is passed down into Item as a prop. it's either true or false. When it changes, toggleDisplay is called and changes state in my hook:
useEffect(() => {
toggleDisplay(!display); //this is just a useState hook call
}, [shouldShowItem]); //PS: I'm aware that I don't need this extra step here, but my actual code is a bit more complicated, so I just simplified it here.
My Problem is, that I have one single shouldShowItem property in redux, not one shouldShowItem for each item. I don't want to move this property into the redux-state for each and every item.
Problem:
The problem with my construction however is that shouldShowItem is being saved, which means that if I toggle it at time X for item Y, and then my Item Z also re-renders as a result of an unrelated event, it will re-render with an updated shouldShowItem state, - although that state change was intended for Item X.
Essentially, I am saving the state of shouldShowItem in redux while I just need a toggle, that I can dispatch once, that works on the current Item, and then isn't read / needed anymore. I want to basically dispatch a toggle, - I don't care about the state of each item within redux, I just care that it's toggled once.
Suggestions?
Edit: More Code
Item:
const Item = ({shouldShowItem, itemText, updateCurrentItem})=>
{
const [display, toggleDisplay] = useState(false);
useEffect(() => {
if (isSelected) {
toggleDisplay(!display);
}
}, [shouldShowItem]);
return (
<div
onClick={() => {
toggleDisplay(!display);
updateCurrentItem(item._id);
}}
>
{display && <span>{itemText}</span>}
</div>
);
}
Mapping through item list:
allItems.map((item, i) => {
return (
<Item
key={i}
shouldShowItem={this.props.shouldShowItem}
itemText={item.text}
updateCurrentItem={this.props.updateCurrentItem}
);
});
The props here come from redux, so shouldShowItem is a boolean value that lies in redux, and updateCurrentItem is an action creator. And well, in redux i simply just toggle the shouldShowItem true & false whenever the toggle action is dispatched. (The toggle action that sets & unsets true/false of shouldShowItem is in some other component and works fine)
instead of a boolean shouldShowItem, why not convert it into an object of ids with boolean values:
const [showItem, setShowItem] = useState({id_1: false, id_2: false})
const toggleDisplay = id => {
setShowItem({...showItem, [id]: !showItem[id]})
updateCurrentItem(id)
}
allItems.map((item, i) => {
return (
<Item
key={i}
shouldShowItem={showItem[item._id]}
itemText={item.text}
updateCurrentItem={toggleDisplay}
);
});
const Item = ({shouldShowItem, itemText, updateCurrentItem}) => {
return (
<div
onClick={() => toggleDisplay(item._id)}
>
{shouldShowItem && <span>{itemText}</span>}
</div>
)
}

Toggle view dynamically on click ReactJs

I have mapped list of data from JSON. When I clicked on of the item it should open a crawl with additional details from the same JSON file. I am able to map everything one I clicked bit I was not able to toggle. How do I do toggling.
This is my render method
render() {
return (
<div>
<h1>API</h1>
<div>
{this.state.apis.map(api => (
<div
key={api.id}
id={api.id}
onClick={this.handleCrawl}>
{api.title}
</div>
))}
</div>
<div>
{this.state.apis.map(api => (
<div
key={api.id}
id={api.id}>
{this.state.showCrawl[api.id] && (
<SwaggerUI url={api.opening_crawl}/>
)}
</div>
))}
</div>
</div>
);
}
This is the method for toggling. When I clicked an item the SwaggerUI component shows up and If I clicked the same link it hides.
The problem is if I clicked the 2nd link 1st link still shows. I need other view to be closed.
handleCrawl = e => {
const { id } = e.target;
this.setState(current => ({
showCrawl: { ...current.showCrawl, [id]: !current.showCrawl[id] }
}));
};
just don't spread the previous state's props.
try this:
handleCrawl = e => {
const { id } = e.target;
this.setState(current => ({
showCrawl: { [id]: !current.showCrawl[id] }
}));
};
Because in your code:
initial state:
{showCrawl: {}}
Say first time you click the first one(id: 1), your state become:
{showCrawl: {1: true}}
then u click the second one(id: 2)
{showCrawl: {1: true, 2: true}}
That's not your expected. Right?
So just don't spread the property, it should be going well.
In general, you can show or hide an element in a react component like this:
{this.state.showComponent ? (<Component/>) : (null)}
as an alternative, you can control the hiding/showing of the element in the component itself, with a show prop:
<Component show={this.state.showComponent} />
-- edit
I think I misunderstood your problem. Your problem is that you only want SwaggerUI to show for one thing at a time, but it's showing for multiple.
This is because of the way you designed your function,
handleCrawl = e => {
const { id } = e.target;
this.setState(current => ({
showCrawl: { ...current.showCrawl, [id]: !current.showCrawl[id] }
}));
};
You're only ever ADDING ids to showCrawl, not changing the ids that you toggled previously. You'll have to fix that function

How to show table row depending on user role in React

I create table that render data which receive from server. But in this table I have cols which not all user should to see. This is my code:
class TableBody extends React.Component {
state = {
columns: {
id: props => (
<td key="id">
{props._id}
</td>
),
title: props => (
<td key="title">
{props.title}
</td>
),
manager: props => (
<td key="manager">
{props.manager}
</td>
)
},
hiddenColumns: {
user: ['manager']
}
}
In state I init my columns and add columns restrictions for user (he can not see manager column). In render I do next:
render() {
const hiddenColumns = this.state.hiddenColumns[this.props.role] || [];
const columns = Object.keys(this.state.columns).filter(key => {
return hiddenColumns.indexOf(key) === -1
});
return this.props.companies.map(company => (
<tr key={offer._id}>
{columns.map(element => this.state.columns[element](company))}
</tr>
));
}
I get hidden columns for current user and filter key in columns. After this I use map to go over data which receive from server and inside map I go over for each filtered columns and send element (props).
In the future, more columns will be added to this table and make this:
{columns.map(element => this.state.columns[element](company))}
will not be effective.
Maybe I can create main template and after init remove columns which user should not to see, but I don't know how.
Please help me
Thank you
I think you are doing this completely wrong. you should never filter data specific to user role on client side.
Ideally such data should be filtered on server side and then send to client only role specific data.
With your current approach user can simply inspect browser network tab and read all other restricted columns.

Categories