Toggle view dynamically on click ReactJs - javascript

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

Related

Creating like button for multiple items

I am new to React and trying to learn more by creating projects. I made an API call to display some images to the page and I would like to create a like button/icon for each image that changes to red when clicked. However, when I click one button all of the icons change to red. I believe this may be related to the way I have set up my state, but can't seem to figure out how to target each item individually. Any insight would be much appreciated.
`
//store api data
const [eventsData, setEventsData] = useState([]);
//state for like button
const [isLiked, setIsLiked] = useState(false);
useEffect(() => {
axios({
url: "https://app.ticketmaster.com/discovery/v2/events",
params: {
city: userInput,
countryCode: "ca",
},
})
.then((response) => {
setEventsData(response.data._embedded.events);
})
.catch((error) => {
console.log(error)
});
});
//here i've tried to filter and target each item and when i
console.log(event) it does render the clicked item, however all the icons
change to red at the same time
const handleLikeEvent = (id) => {
eventsData.filter((event) => {
if (event.id === id) {
setIsLiked(!isLiked);
}
});
};
return (
{eventsData.map((event) => {
return (
<div key={event.id}>
<img src={event.images[0].url} alt={event.name}></img>
<FontAwesomeIcon
icon={faHeart}
className={isLiked ? "redIcon" : "regularIcon"}
onClick={() => handleLikeEvent(event.id)}
/>
</div>
)
`
Store likes as array of ids
const [eventsData, setEventsData] = useState([]);
const [likes, setLikes] = useState([]);
const handleLikeEvent = (id) => {
setLikes(likes.concat(id));
};
return (
<>
{eventsData.map((event) => {
return (
<div key={event.id}>
<img src={event.images[0].url} alt={event.name}></img>
<FontAwesomeIcon
icon={faHeart}
className={likes.includes(event.id) ? "redIcon" : "regularIcon"}
onClick={() => handleLikeEvent(event.id)}
/>
</div>
);
})}
</>
);
Your issue is with your state, isLiked is just a boolean true or false, it has no way to tell the difference between button 1, or button 2 and so on, so you need a way to change the css property for an individual button, you can find one such implementation by looking Siva's answer, where you store their ids in an array

dynamic Button class does not update after the array change

So I have an array of objects. I iterate through this array and create a button for each object.
When a button is pressed that object of the button pressed has a value "active" that will be set to true. when another button is pressed its "active" value is now true all all the other ones are turned to false.
it looks like this
myarray.map(item =>
<Button
className={item.active? "btn-active" : "btn-disabled"}
onClick={() => setActive(item);
}}
>
{item.active? "Checking..." : "Start"}
</Button>
)
The behavior I expect is when a button is pressed it turns to action, and all the rest remain inactive, when a new button is pressed the new button is now active and all the rest are disabled. only one active button at a time.
However, the issue I am having is when a new button is pressed it turns to active, but the old one does not change class and stays active also even though it "active" property is set to false.
Any idea how can I fix this behavior?
Without a full picture of how you are using state, here is a working example. Another issue I seen is that you are missing a key on your mapped jsx element.
It's possible you are not mutating myarray statefully.
import "./styles.css";
import React from "react";
export default function App() {
const [myarray, setMyarray] = React.useState([
{ id: 1, active: false },
{ id: 2, active: false }
]);
const setActive = (id) => {
setMyarray((prev) =>
prev.map((item) => {
if (item.id === id) {
return { ...item, active: true };
}
return { ...item, active: false };
})
);
};
return (
<div className="App">
{myarray.map((item) => (
<button
key={`button-${item.id}`}
className={item.active ? "btn-active" : "btn-disabled"}
onClick={() => setActive(item.id)}
>
{item.active ? "Checking..." : "Start"}
</button>
))}
</div>
);
}
https://codesandbox.io/s/flamboyant-shirley-i24v0z

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

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

How to add json data to an array onClick?

I'm new to React/using API json data in a project so I'm having a little trouble. I've created a function where a user can type in a search query and a list of devices associated with their query will show up. These device names are fetched from an API. I'm trying to make it so that when the plus sign next to a device is clicked, it adds this device to a new array that is then displayed to the screen.
I'm not 100% familiar with the concept of state in React and I think that's where my issue is (in the addDevice function). It's partially working, where I click the device and it displays at the bottom, but when I click another device, instead of adding to the list, it just replaces the first device.
class App extends React.Component {
state = {
search: "",
devices: [],
bag: []
};
addDevice = (e, data) => {
console.log(data);
const newData = [this.state.devices.title];
this.setState({
bag: newData.concat(data)
});
};
onChange = e => {
const { value } = e.target;
this.setState({
search: value
});
this.search(value);
};
search = search => {
const url = `https://www.ifixit.com/api/2.0/suggest/${search}?doctypes=device`;
fetch(url)
.then(results => results.json())
.then(data => {
this.setState({ devices: data.results });
});
};
componentDidMount() {
this.search("");
}
render() {
return (
<div>
<form>
<input
type="text"
placeholder="Search for devices..."
onChange={this.onChange}
/>
{this.state.devices.map(device => (
<ul key={device.title}>
<p>
{device.title}{" "}
<i
className="fas fa-plus"
style={{ cursor: "pointer", color: "green" }}
onClick={e => this.addDevice(e, device.title)}
/>
</p>
</ul>
))}
</form>
<p>{this.state.bag}</p>
</div>
);
}
}
I want it to display all the devices I click one after another, but right now each device clicked just replaces the previous one clicked
I think you're close. It appears that you are getting the devices array and the bag array mixed up.
I'd suggest using Array.from to create a copy of your state array. Then push the new item into the array. Concat is used to merged two arrays.
addDevice = (e, data) => {
// create new copy of array from state
const newArray = Array.from(this.state.bag);
// push new item into array
newArray.push(data);
// update the state with the new array
this.setState({
bag: newArray
});
}
Then if you want to show the device titles as a comma separated string, you could just do:
<p>{this.state.bag.join(', ')}</p>
Hope this helps.
The issue is with your addDevice method and specifically with how you create newData. You set newData to [this.state.devices.title], which evaluates to [undefined] since this.state.devices is an array and therefore has no attribute called title. Therefore, the updated value of state.bag will be [undefined, data], and only render as data which is the title of the most recently clicked device.
I think what you mean to do here is append the title of the clicked device to the array state.bag. You can do this with an addDevice method like this:
addDevice = (e, data) => {
console.log(data);
const newBag = this.state.bag.concat(data);
this.setState({
bag: newBag
});
};
Though a better practice way of updating state.bag would make use of the functional form of setState, and the spread operator (...) is more common for this sort of stuff than using concat. Also renaming data to something more explanatory (like deviceTitle) would be helpful here. Example:
addDevice = (e, deviceTitle) => {
this.setState(prevState => ({
bag: [...prevState.bag, deviceTitle],
});
}
Edit:
If you want to add functionality to remove devices from state.bag, you can create a method called removeDevice and add a button next to each bag item when rendering.
For example:
removeDevice = (e, deviceTitle) => {
this.setState(prevState => ({
bag: prevState.bag.filter(d => d !== deviceTitle),
});
}
Then in your render method you would have something like this:
<ul>
{this.state.bag.map(deviceTitle => (
<li>
<span>{ deviceTitle }</span>
<button onClick={ e => this.removeDevice(e, deviceTitle) }>remove</button>
</li>
))}
</ul>

React pattern for List Editor Dialog

I'd like to know what's the best pattern to use in the following use case:
I have a list of items in my ItemList.js
const itemList = items.map((i) => <Item key={i}></Item>);
return (
<div>{itemList}</div>
)
Each of this Items has an 'EDIT' button which should open a dialog in order to edit the item.
Where should I put the Dialog code?
In my ItemList.js => making my Item.js call the props methods to open the dialog (how do let the Dialog know which Item was clicked? Maybe with Redux save the id of the item inside the STORE and fetch it from there?)
In my Item.js => in this way each item would have its own Dialog
p.s. the number of items is limited, assume it's a value between 5 and 15.
You got a plenty of options to choose from:
Using React 16 portals
This option let you render your <Dialog> anywhere you want in DOM, but still as a child in ReactDOM, thus maintaining possibility to control and easily pass props from your <EditableItem> component.
Place <Dialog> anywhere and listen for special app state property, if you use Redux for example you can create it, place actions to change it in <EditableItem> and connect.
Use react context to send actions directly to Dialog, placed on top or wherever.
Personally, i'd choose first option.
You can have your <Dialog/> as separate component inside application's components tree and let it to be displayed in a case if your application's state contains some property that will mean "we need to edit item with such id". Then into your <Item/> you can just have onClick handler that will update this property with own id, it will lead to state update and hence <Dialog/> will be shown.
UPDATED to better answer the question and more completely tackle the problem. Also, followed the suggestion by Pavlo Zhukov in the comment below: instead of using a function that returns functions, use an inline function.
I think the short answer is: The dialog code should be put alongside the list. At least, this is what makes sense to me. It doesn't sound good to put one dialog inside each item.
If you want to have a single Dialog component, you can do something like:
import React, { useState } from "react";
import "./styles.css";
const items = [
{ _id: "1", text: "first item" },
{ _id: "2", text: "second item" },
{ _id: "3", text: "third item" },
{ _id: "4", text: "fourth item" }
];
const Item = ({ data, onEdit, key }) => {
return (
<div key={key}>
{" "}
{data._id}. {data.text}{" "}
<button type="button" onClick={onEdit}>
edit
</button>
</div>
);
};
const Dialog = ({ open, item, onClose }) => {
return (
<div>
<div> Dialog state: {open ? "opened" : "closed"} </div>
<div> Dialog item: {JSON.stringify(item)} </div>
{open && (
<button type="button" onClick={onClose}>
Close dialog
</button>
)}
</div>
);
};
export default function App() {
const [isDialogOpen, setDialogOpen] = useState(false);
const [selectedItem, setSelectedItem] = useState(null);
const openEditDialog = (item) => {
setSelectedItem(item);
setDialogOpen(true);
};
const closeEditDialog = () => {
setDialogOpen(false);
setSelectedItem(null);
};
const itemList = items.map((i) => (
<Item key={i._id} onEdit={() => openEditDialog(i)} data={i} />
));
return (
<>
{itemList}
<br />
<br />
<Dialog
open={isDialogOpen}
item={selectedItem}
onClose={closeEditDialog}
/>
</>
);
}
(or check it directly on this CodeSandbox)

Categories