I have a map that render few items and one of its line is below
<a onClick={()=> this.setState({"openDeleteModal":true)}>Delete</a>
Obviously I want to open a modal when user click the delete, but I have to pass a few things like the name of the item, id of the item to perform the deletion. How can I pass says the name to the modal?
I can bind the obj name to a like this
Delete
Am I on the right track?
When working on React applications, try not to think in terms of passing values to other components, but rather updating state that your components are exposed to.
In your example, assuming your modal component is a child of the same component your list of a tags belongs to, you could set the values you are interested in exposing to the modal on the state, as well as updating the property that signals whether the modal is open or not. For example:
class Container extends React.Component {
constructor(props) {
super(props)
this.state = {
openDeleteModal: false,
activeItemName: '', //state property to hold item name
activeItemId: null, //state property to hold item id
}
}
openModalWithItem(item) {
this.setState({
openDeleteModal: true,
activeItemName: item.name,
activeItemId: item.id
})
}
render() {
let buttonList = this.props.item.map( item => {
return (<button onClick={() => this.openModalWithItem(item)}>{item.name}</button>
});
return (
<div>
{/* Example Modal Component */}
<Modal isOpen={this.state.openDeleteModal}
itemId={this.state.activeItemId}
itemName={this.state.activeItemName}/>
{ buttonList }
</div>
)
}
}
Copying over my answer from How to pass props to a modal
Similar scenario
constructor(props) {
super(props)
this.state = {
isModalOpen: false,
modalProduct: undefined,
}
}
//****************************************************************************/
render() {
return (
<h4> Bag </h4>
{this.state.isModalOpen & (
<Modal
modalProduct={this.state.modalProduct}
closeModal={() => this.setState({ isModalOpen: false, modalProduct: undefined})
deleteProduct={ ... }
/>
)
{bag.products.map((product, index) => (
<div key={index}>
<div>{product.name}</div>
<div>£{product.price}</div>
<div>
<span> Quantity:{product.quantity} </span>
<button onClick={() => this.props.incrementQuantity(product, product.quantity += 1)}> + </button>
<button onClick={() => this.decrementQuantity(product)}> - </button> // <----
</div>
</div>
))}
)
}
//****************************************************************************/
decrementQuantity(product) {
if(product.quantity === 1) {
this.setState({ isModalOpen: true, modalProduct: product })
} else {
this.props.decrementQuantity(product)
}
}
Try this: this is the form which has the button, and is a child component of some other component that passes the handleButtonAction method as props, and the button takes the input data and invokes this parent component method
handleSubmit = (e) => {
e.preventDefault();
const data = e.target.elements.option.value.trim();
if (!data) {
this.setState(() => ({ error: 'Please type data' }));
} else {
this.props.handleButtonAction(data, date);
}
}
{this.state.error && <p>{this.state.error}</p>}
<form onSubmit={this.handleSubmit}>
<input type="text" name="option"/>
<div>
<button>Get data</button>
</div>
</form>
The parent component:
handleButtonAction = (data) => {
axios.get(`http://localhost:3000/someGetMethod/${data}`).then(response => {
const resData = response.data;
this.setState({
openModal: true,
status: response.status,
data: resData
});
}).catch((error) => {
if (error.message.toLowerCase() === 'network error') {
this.setStateWithError(-1, {});
}
else { // not found aka 404
this.setStateWithError(error.response.status, '', {currency, date: ddat});
}
});
}
Related
I have been attempting to toggle a class on click so that when I click on one of the mapped items in my Tasks component, I add the 'complete' class and put a line through that item (crossing items off of a todo list). However with my current code set up, when I click on one element to add the class, all the other elements get crossed out as well and vice versa.
Here is my current setup. The class 'complete' is what will add a line through one of the mapped items in the Tasks component.
import { Container, Row} from 'react-bootstrap';
import {Link} from 'react-router-dom';
import axios from 'axios';
const List = (props) =>{
return(
<div>
<Link style={{textDecoration:'none'}} to={`/lists/${props.listId}`} > <p className="list-item">{props.item}</p></Link>
</div>
)
}
const Tasks = (props) =>{
return(
<div onClick={props.onClick} className={props.className} >
<div className='task-item' >
<p >{props.item}</p>
</div>
</div>
)
}
export default class Display extends Component {
constructor(props){
super(props)
this.onCompletedTask = this.onCompletedTask.bind(this);
this.state = {
list: [],
tasks:[],
complete:false
}
}
componentWillUpdate(nextProps){
axios.get(`http://localhost:8080/lists/${this.props.match.params.listId}`)
.then(response =>{
this.setState({
tasks:response.data
})
})
}
componentDidMount(){
axios.get('http://localhost:8080/lists')
.then(response=>{
this.setState({
list:response.data
})
})
.catch(error =>{
console.log(error)
});
}
onCompletedTask(item){
this.setState({ complete: !this.state.complete});
}
listCollection(){
return(
this.state.list.map(item=>{
return(<List item = {item.title} listId={item._id} key = {item._id} />)
})
)
}
taskCollection(){
return(
this.state.tasks.map((item, index) =>{
return(<Tasks onClick = {()=>this.onCompletedTask(item)} className={this.state.complete ? 'complete': ''} item={item.task} key={index}/>)
})
)
}
render() {
return (
<div id='main' >
<Container>
<Row>
<div className="sidebar">
<h1 style={{fontSize:"25pt"}}>Lists</h1>
<div className="list-menu">
{this.listCollection()}
</div>
<form action='/new-list' method='GET'>
<div style={{textAlign:'center'}}>
<button className='list-button' style={{fontSize:'12pt', borderRadius:'5px'}}>
+ New List
</button>
</div>
</form>
</div>
<div className='tasks'>
<h1 style={{fontSize:'25pt'}}>Tasks</h1>
{this.taskCollection()}
<form action={`/lists/${this.props.match.params.listId}/new-task`} method='GET'>
<button className='task-button'>
+
</button>
</form>
</div>
</Row>
</Container>
</div>
)
}
}
Your state holds only a single completed value, which OFC toggle all tasks. You could instead store a map of completed tasks.
this.state = {
list: [],
tasks: [],
complete: {}, // <--- use empty object as simple map object
}
Update onCompletedTask to store some uniquely identifying property of a task, like an id field
onCompletedTask(item){
this.setState(prevState => ({
completed: {
...prevState.completed, // <--- spread existing completed state
[item.id]: !prevState.completed[item.id] // <--- toggle value
},
}));
}
Update. taskCollection to check the completed map by id
taskCollection = () => {
const { completed, tasks } = this.state;
return tasks.map((item, index) => (
<Tasks
onClick={() => this.onCompletedTask(item)}
className={completed[item.id] ? "complete" : ""} // <--- check completed[item.id]
item={item.task}
key={index}
/>
))
};
i'm using React to create an App where i can see the flags and various infos about every country. I'm using an API to fetch the data and i've already mapped them all with a grid. That's my code so far:
class App extends React.Component{
constructor (props){
super (props);
this.state={
countries : [],
info: ""
}
}
componentDidMount(){
axios.get(`https://restcountries.eu/rest/v2/all`)
.then(res => {
const data = res.data;
console.log(data);
this.setState({
countries : data
})
this.showInfo = this.showInfo.bind(this)
})
}
showInfo (e) {
console.log(e.target.key);
}
render() {
return (
<div className="container">
{this.state.countries.map(country=>
<Country name={country.name}
key={country.name}
population ={country.population}
region={country.region}
capital={country.capital}
flag={country.flag}
showInfo={this.showInfo}
/>
)}
</div>
)
}
}
export default App;
And this is my Country-item component:
const Country = ({name, population, region, capital, flag, showInfo})=>{
return (
<div onClick={showInfo} className="country-item">
<div className="img">
<img src={flag}/>
</div>
<p>{name}</p>
<p>population: {population}</p>
<p>Region: {region}</p>
<p>Capital: {capital}</p>
</div>
)
}
export default Country
So far for now i have something like this:
enter image description here
Now i would like to click on each country box item and display that clicked data inside a modal popUp. If i create a modal and i will map it, of course on click i will have all of them displayed in once. how can i pass the props of that box i clicked on the modal component? i created a function trying to capture for example the key props, but i didn't suceed. What's the best strategy? thank you very much for the help
Attach an onClick handler to each country. When it's clicked, save the country name to the state of the containing component. Render the modal with content only from the country clicked:
class App extends React.Component{
constructor (props){
super (props);
this.state={
countries : [],
info: "",
clicked: ''
}
this.countryClickHandler = e => {
this.setState({clicked: country.name}, () => {
window.addEventListener('click', this.closeCountryPopup)
})
}
this.closeCountryPopup = e => {
this.setState({clicked: ''}, () => {
window.removeEventListener('click', this.closeCountryPopup)
})
}
}
componentDidMount(){
axios.get(`https://restcountries.eu/rest/v2/all`)
.then(res => {
this.setState({
countries : res.data
})
})
}
renderPopup() {
// if the clicked flag is falsy, null, or an empty string, don't render anything
if(!this.state.clicked || this.state.clicked === null || !this.state.clicked.length) return null
// otherwise, render the only clicked country by filtering it by matching it with the string in the state
const clickedCountry = this.state.countries.find(country => country.name === this.state.clicked)
return (
<div className="popup_container">
<Country
name={clickedCountry.name}
key={clickedCountry.name}
population ={clickedCountry.population}
region={clickedCountry.region}
capital={clickedCountry.capital}
flag={clickedCountry.flag}
/>
</div>
)
}
render() {
return (
<div className="container">
{this.state.countries.map(country =>
<div onClick={this.countryClickHandler}>
<Country
name={country.name}
key={country.name}
population ={country.population}
region={country.region}
capital={country.capital}
flag={country.flag}
/>
</div>
)}
{ this.renderPopup() }
</div>
)
}
}
export default App;
Your App component should maintain the state of which country should be displayed in a modal. More specifically App component will save in it's state if a modal should be displayed and which country to be displayed in the modal.
The showInfo prop that you pass to the Country component, should notify App component when a country is clicked.
I have created a representative example on CodePen.
class App extends React.Component {
constructor () {
super();
this.state = {
showModal: false,
selectedCountry: {},
countries: [
{name: "Germany", continent: "Europe"},
{name: "South Korea", continent: "Asia"},
{name: "New Zealnd", continent: "Australia"}
]
};
}
handleCloseModal = () => {
this.setState({
showModal: false
});
}
showInfo = (name) => {
this.setState({
selectedCountry: this.state.countries.find(it => it.name===name),
showModal: true
});
}
render () {
return (
<div>
{
this.state.countries.map((country) => <Country
name={country.name}
continent={country.continent}
showInfo={this.showInfo}
/>
)
}
<ReactModal
isOpen={this.state.showModal}
contentLabel={this.state.selectedCountry.name}
>
<div className="modal">
<div>{this.state.selectedCountry.name}</div>
<div>{this.state.selectedCountry.continent}</div>
</div>
<button onClick={this.handleCloseModal}>Close Modal</button>
</ReactModal>
</div>
);
}
}
const Country = (props) => {
return (
<div className="country" onClick={() => props.showInfo(props.name)}>
<div>{props.name}</div>
<span>-</span>
<div>{props.continent}</div>
</div>
)
};
I want to paginate the reults of an Api call.
I am making an Api Call by using Axios like this
apiCall() {
const API = `http://www.omdbapi.com/`;
axios.get(API, {
params: {
apikey: process.env.REACT_APP_MOVIECALL_API_KEY,
type: 'movie',
s: 'superhero',
page: this.state.pageCount
}
})
.then(res => {
const superheroes = res.data.Search
const totalResults= parseInt(res.data.totalResults)
this.setState({
totalResults
});
this.setState({
superheroes
})
})
.catch((error) => {
console.log(error);
});
}
When the component is mounted the function called, as such
componentDidMount()
{
this.apiCall();
}
In the render function I map over each search result (in the api call the s param is the search option)
and for each result I display a button, that when clicked displays the related info of that movie.
The api by default displays 10 results per call but this particular search has 123 results in total.
Now by updating the param page in the call which I've set to this.state.pageCount
it displays 10 different movies relating to that page, at the moment I hardcode the pageCount inside the state to make sure it works and the corresponding page number shows the right list of 10 movies.
Now I would like to paginate the results by updating the page number, so when you click on next or the number 3/4/5 then the component loads the correct corresponding results, I've tried a couple of option relating to react but they somehow don't update the page number.
If someone could point me in the right direction or knows a simple solution, I am all ears.
the following code is the whole component, to get an idea of what I am trying to do.
What I have so far seems to be working, so what I am asking is for a
simpler more elegant way of doing pagination for this particular
situation.
export class MovieDetails extends Component {
constructor(props){
super(props)
this.state = {
superheroes: [],
clicked: false,
activeHero: {},
pageCount: 11,
totalResults: null,
currentPage: 1
}
this.handleClick = this.handleClick.bind(this);
}
handleClick(hero) {
const checkActive = this.state.activeHero.imdbID === hero.imdbID
const activeHero = {...hero, active: !checkActive}
this.setState({
clicked: !this.state.clicked,
activeHero
})
}
apiCall() {
const API = `http://www.omdbapi.com/`;
axios.get(API, {
params: {
apikey: process.env.REACT_APP_MOVIECALL_API_KEY,
type: 'movie',
s: 'superhero',
page: this.state.pageCount
}
})
.then(res => {
const superheroes = res.data.Search
const totalResults = parseInt(res.data.totalResults)
this.setState({
totalResults
});
this.setState({
superheroes
})
})
.catch((error) => {
console.log(error);
});
}
componentDidMount() {
this.apiCall();
}
handlePageChange = (page, e) => {
this.setState({
currentPage: page
});
this.apiCall(this.setState({pageCount: page}))
};
render() {
const {superheroes, currentPage } = this.state
return (
<div>
{
superheroes.map((hero, i) =>
<div className="Results" key={i}>
<button onClick={() => {this.handleClick(hero)}}>
<h1>{hero.Title}</h1>
{
this.state.clicked && this.state.activeHero.imdbID === hero.imdbID
? <ul>
{<div key={i}>
Movie Title: <h2> {hero.Title}</h2>
Year of Release: <h2>{hero.Year}</h2>
ID: <h2>{hero.imdbID}</h2>
<div><img className="Poster" alt="movieposter" src={hero.Poster}/></div>
</div>
}
</ul>
: null
}
</button>
</div>)
}
<div className="Pagination">
<Pagination
total={this.state.totalResults}
limit={10}
pageCount={this.state.pageCount}
currentPage={currentPage}
>
{({
pages,
currentPage,
hasNextPage,
hasPreviousPage,
previousPage,
nextPage,
totalPages,
getPageItemProps
}) => (
<div>
<button
{...getPageItemProps({
pageValue: 1,
onPageChange: this.handlePageChange
})}
>
first
</button>
{hasPreviousPage && (
<button
{...getPageItemProps({
pageValue: previousPage,
onPageChange: this.handlePageChange
})}
>
{'<'}
</button>
)}
{pages.map(page => {
let activePage = null;
if (currentPage === page) {
activePage = { backgroundColor: '#fdce09' };
}
return (
<button
{...getPageItemProps({
pageValue: page,
key: page,
style: activePage,
onPageChange: this.handlePageChange
})}
>
{page}
</button>
);
})}
{hasNextPage && (
<button
{...getPageItemProps({
pageValue: nextPage,
onPageChange: this.handlePageChange
})}
>
{'>'}
</button>
)}
<button
{...getPageItemProps({
pageValue: totalPages,
onPageChange: this.handlePageChange
})}
>
last
</button>
</div>
)}
</Pagination>
</div>
</div>
);
}
}
In your axios.get function you are sending page: this.state.pageCount, however in your handlePageChange function you are setting state.currentPage which doesn't seem right to me.
I'm also a bit confused about the onPageChange event on <button />. Is this button a custom component you are importing (in which case it should be capatalised so that it's clear) or is it a HTML button? If it's a HTML button then you need to use the onClick event which will pass the event as and argument to the handlePageChange function. I'm guessing it's custom though from the props you're passing it so just worth checking that it's sending the page value through correctly.
A single click on a button updates all the buttons but I want to change the state of that particular clicked button. Please check the image links below and the code.
import React from 'react';
import './MenuCard.css';
class MenuCard extends React.Component {
constructor(props) {
super(props);
this.state = {
showButton: false,
hideButton: true,
aValue: 1,
breads: [],
category: [],
ids: 1,
btnVal: 'Add'
};
}
onKeyCheck = (e) => {
this.state.breads.map(filt => {
if (filt.id === e.target.id) {
console.log(e.target.id + ' and ' + filt.id)
return (this.setState({showButton: !this.state.showButton, hideButton: !this.state.hideButton}));
}
})
}
onShowButton = () => {
this.setState({showButton: !this.state.showButton, hideButton: !this.state.hideButton})
}
onValueIncrease = () => {
this.setState({aValue: this.state.aValue + 1});
}
onValueDecrease = () => {
this.setState({aValue: this.state.aValue - 1});
}
componentDidMount() {
fetch('http://localhost:3000/menu/food_category', {
method: 'get',
headers: {'content-type': 'application/json'}
})
.then(response => response.json())
.then(menudata => {
this.setState({category: menudata.menu_type})
console.log(this.state.category)
})
fetch('http://localhost:3000/menu', {
method: 'get',
headers: {'content-type': 'application/json'}
})
.then(response => response.json())
.then(menudata => {
this.setState({breads: menudata })
})
}
render() {
return (
<div>
{this.state.category.map(types => {
return (<div>
<div className="menu-head">{types}</div>
< div className="container-menu">
{this.state.breads.map((d, id)=> {
if (d.category === types) {
return (
<div>
<div className="content" key={id} id={d.id}>
<div className="items"> {d.item_name}</div>
<div className="prices"> {d.price} Rs.</div>
{this.state.showButton ?
<div>
<button
className="grp-btn-minus"
onClick={this.state.aValue <= 1 ?
() => this.onShowButton() :
() => this.onValueDecrease()}>-
</button>
<input className="grp-btn-text" type="text"
value={this.state.aValue} readOnly/>
<button id={d.id}
className="grp-btn-plus"
onClick={() => this.onValueIncrease()}>+
</button>
</div> :
<button id={d.id} key={id}
onClick={ this.onKeyCheck}
className="add-menu-btn">
add
</button>
}
</div>
</div>
)
}
})}
</div>
</div>)
})}
</div>
)
}
}
export default MenuCard;
This is the first image of multiple rendering of component Add buttons
Here is the problem that all buttons get updated on single click
You're using an array of items but refering to a single, shared value in handlers. De facto you're using a few shared values: showButton, hideButton, aValue), 2/3 unnecessary ;)
First - aValue for each item should be stored in a structure - array or object. It could be an order = {} - object with id-keyed properties with amounts as values like this:
order = {
'masala_id': 1,
'kebab_id' : 2
}
Event handler (for 'add') should check if id for choosen product already exist in order object (as property name) and update amount (+/-) or create new one with 1 value (and remove property when decreased amount = 0).
In practice order should also contain a price - it seams like duplicating data but it will be much easier to count total order value.
order = {
'masala_id': {
'amount': 1,
'price': 20,
},
'kebab_id' : {
'amount': 2,
'price': 180,
}
}
Item doesn't need to be a component but it's much easier to maintain it, keep it readable etc.
This way we can simply pass already ordered amount and conditionally render buttons:
<Product id={d.id}
name={d.item_name}
price={d.price}
amount={order[d.id] ? order[d.id].amount : 0 }
amountHandler={this.changeAmountHandler}
/>
Product should be slightly improved and simplified (f.e. key is needed on top div):
class Product extends React.Component {
render () {
const (id, name, price, amount, amountHandler} = this.props;
const showIncrease = !!amount; // boolean, it also means "don't show add button"
return (
<div key={id} >
<div className="content">
<div className="items">{name}</div>
<div className="prices">{price} Rs.</div>
{showIncrease ?
<div>
<button
className="grp-btn-minus"
onClick={(e) => { amountHandler(e, id, -1) }}
>-</button>
<input className="grp-btn-text" type="text"
value={amount}
readOnly/>
<button
className="grp-btn-plus"
onClick={(e) => { amountHandler(e, id, 1) }}
>+</button>
</div> :
<button
onClick={(e) => { amountHandler(e, id, 1) }}
className="add-menu-btn"
>add</button>
}
</div>
</div>
)}}
This way you can handle all events in one handler, keep entire order state in main component... in case of performance problems just use PureComponent.
It looks like all the buttons are sharing the same state. You could try breaking the button up into its own component, and then move the state that button needs into there. That way when you click a button the state of that one particular button is updated, and not the state of the parent component that contains all the buttons.
You can see demo here. Try to click "Edit" button and change the value of input field.
In the parent component, it pass an array of objects to its child. Inside the child component, one of objects can be passed into its state, named editedTodo. But, strangely, the prop is changed when editedTodo is changed.
This is not what I want. Anyone can help me solve this?
Here is the todo Component:
import React from "react";
export default class extends React.Component {
state = {
editedTodo: null
};
toggleEditTodo = (todo = null) => {
this.setState({ editedTodo: todo });
};
onChangeTodoText = text => {
this.setState(prevState => ({
editedTodo: Object.assign(prevState.editedTodo, { text })
}));
};
submitTodoForm = e => {
e.preventDefault();
this.props.saveEditedTodo(this.state.editedTodo);
this.setState({
editedTodo: null
});
};
render() {
const { editedTodo } = this.state;
const { todos } = this.props;
return (
<ul>
<li>{JSON.stringify(todos)}</li>
{todos.map(todo => (
<li key={todo.id}>
{todo === editedTodo ? (
<form onSubmit={this.submitTodoForm}>
<input
autoFocus
value={editedTodo.text}
onChange={e => this.onChangeTodoText(e.target.value)}
/>
<button type="submit">Save</button>
<button type="button" onClick={this.toggleEditTodo}>
Cancel
</button>
</form>
) : (
<span>
{todo.text}
<button onClick={() => this.toggleEditTodo(todo)}>Edit</button>
</span>
)}
</li>
))}
</ul>
);
}
}
https://codesandbox.io/s/3q1k4m3vm5
Here is the working version.
The problem was with this.setState({ editedTodo: todo }). You are assigning the same copy of todo from the props to the state. So it is referencing the same item. Make sure you are never mutating your props directly, it is an anti-pattern.