I was playing around with React and I have a list of elements like this:
{posts.map((post) => (
<div className ="hidden" key={post.key}></div>
<button onClick = {() => showMore()}>Click to Show</button>
))}
posts is a list of json objects. The idea is that only a short description of the post would show and when the button is clicked, it shows the full body.
posts = [{
"title": "Title",
"description": "",
"thumbnail": 'images/image.png',
"body": "",
"id": 1
}]
I want to show that particular post when the button for it is clicked. This is easy to do with document.getElementById but I'm unable to do this with React.
The only way I can think of is a single useState for all the posts but this is not what I want.
This is different from just showing or hiding a single element because in this case I can't just create a single useState. I guess I could create as many useStates as there are posts but I don't really know how this would work.
Thanks!
The way I would approach this is by creating a Post component which has an internal state:
const Post = ({ post }) => {
const [showMore, setShowMore] = useState(false);
const handleShowMore = () => setShowMore((prevShowMore) => !prevShowMore);
return (
<div class="post" key={post.id}>
<p>{showMore ? post.description : post.description.substring(0, 10)}</p>
<button onClick={handleShowMore}>
Show {showMore ? "Less" : "More"}
</button>
</div>
);
};
If showMore is false (which it is by default), it will only show the first 10 characters of the string. If it is true, it will show the entire string.
Then in your parent component, I'd map over the posts and render the Post component:
const ParentComponent = () => {
return (
<div className="container">
{data.map((post) => (
<Post post={post} />
))}
</div>
);
};
Here is a example: https://codepen.io/AliKlein/pen/KKajvOW
Is this close to what you're trying to accomplish?
Related
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
I found myself stupid and could not get my head around with the logic.
I would like to show a few info triggered byonClick, and only those with matched id.
For example, if I click on the button with id of 1, it would only want to show values in that specific object with id:1 like description, library, etc. Right now, all the data are displayed, and because I am using component in material ui, every drawer component are displayed on top of each other (overlapping).
I know the reason causing this is because I have the drawer component inside the map method, but what could be potential solution?
Below are my simple code,
The structure of my data looks like this,
export const projectdata = [
{
id: 1,
title: "",
subtitle: "",
thumbnail: "",
description:
"",
tech: [
],
library: [""],
WebsiteUrl: "",
GitHubUrl: "",
},
...more data with sequential id number...
]
Original:
const handleDrawerOpen = (event) => () => {
setOpen(!open);
};
...
...
<SwipeableDrawer
open={open}
onClose={handleDrawerOpen(false)}
onOpen={handleDrawerOpen(true)}>
***...only show data with matched id...***
</SwipeableDrawer>
I have already map the array to display the data on webpage inside a div like this,
<div>
{ projectdata?.map(({ id, title, subtitle, thumbnail, description, tech, WebsiteUrl, GitHubUrl, library, index }) => (
<>
<Card>
...some info displayed here...
<button onClick={handleDrawerOpen(id)}></button>
</Card>
<SwipeableDrawer>
***...only show data with matched id...***
</SwipeableDrawer>
</>
))}
<div>
One solution I can think of:
with useState(), pass in id as a prop
const [projectDetailId, setProjectDetailId] = useState(null);
const [projectDetailPage, setProjectDetailPage] = useState(false);
const handleDrawerOpen = (id) => {
setProjectDetailId(id);
setProjectDetailPage(true);
};
const handleDrawerClose = () => {
setProjectDetailId(null);
setProjectDetailPage(false);
};
...
...
{projectDetailId === id ?
<SwipeableDrawer
open={projectDetailPage}
onClose={handleDrawerClose}
></SwipeableDrawer>
: null
}
However, this will trigger strange behavior of the drawer (lagging and no animation), especially with mobile device.
Possibly due to this logic projectDetailId === id ? true : false.
Ok, after your update, your problem is that you create multiple drawers, one for each id. When you click on open and set the open prop to true, all your drawers use this same prop so they all open.
You should move the Drawer out of the for and only create one, and send your object that has the id as a prop to the content of the drawer you have.
something like:
const handleDrawerOpen = (yourData) => () => {
setOpen(!open);
setYourData(yourData)
};
...
// and somewhere on your code
<SwipeableDrawer open={open}>
<SomeComponentToShowTheData data={yourData}/>
</SwipeableDrawer>
I have a list of cities and I'm trying to include a modal with a trash can icon to delete the city next to each item. The problem I have is that the modal seems to pick the last item of the list for EVERY item on the list.
When you click on the icon on any element on the list the confirmation modal always points to the last element on the list and I'm not sure what am I doing wrong. :(
I tried using a Confirm element instead only to find out it's using the modal underneath and I get the same results.
Any gurus around who can help me troubleshoot this will be greatly appreciated!
import React, { useState, useCallback } from "react";
import { List, Icon, Modal, Button } from "semantic-ui-react";
import "semantic-ui-css/semantic.min.css";
const CitiesList = () => {
const [deleteButtonOpen, setDeleteButtonOpen] = useState(false);
const cities = [{ name: "London" }, { name: "Paris" }, { name: "Porto" }];
const handleConfirmDeleteCityModal = useCallback(city => {
console.log("[handleConfirmDeleteCityModal] city", city);
// dispatch(deleteCity(city))
setDeleteButtonOpen(false);
}, []);
const showDeleteCityModal = useCallback(() => {
setDeleteButtonOpen(true);
}, []);
const handleCancelDeleteCityModal = useCallback(() => {
setDeleteButtonOpen(false);
}, []);
return (
<List>
{cities.map(c => (
<List.Item>
<List.Content className="list-item-content">
<List.Header as="h4">{c.name}</List.Header>
</List.Content>
<List.Content floated="left">
<Modal
size="tiny"
open={deleteButtonOpen}
onClose={() => handleCancelDeleteCityModal()}
trigger={
<Icon
name="trash alternate outline"
size="small"
onClick={() => showDeleteCityModal()}
/>
}
>
<Modal.Header>{`Delete City ${c.name}`}</Modal.Header>
<Modal.Content>
<p>Are you sure you want to delete this city?</p>
</Modal.Content>
<Modal.Actions>
<Button negative>No</Button>
<Button
positive
icon="checkmark"
labelPosition="right"
content="Yes"
onClick={() => handleConfirmDeleteCityModal(c)}
/>
</Modal.Actions>
</Modal>
</List.Content>
</List.Item>
))}
</List>
);
};
export default CitiesList;
Here is the example: https://codesandbox.io/s/optimistic-borg-56bwg?from-embed
The problem is this:
<Modal
size="tiny"
open={deleteButtonOpen}
onClose={() => handleCancelDeleteCityModal()}
trigger={
<Icon
name="trash alternate outline"
size="small"
onClick={() => showDeleteCityModal()}
/>
}
>
you use single flag deleteButtonOpen for controlling visibility of all modals. When you set it to true I suppose all modals are opened and you see only the latest one.
Normally I would render single modal and pass as props content of which item I want to show inside.
But if not using separate open flag for each modal should fix it, e.g. https://codesandbox.io/s/vigilant-banzai-byc4t
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
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)