React State Reverts to Previous When Navigating Away - reactjs

I'm currently building a React eCommerce application, and I'm having trouble with getting the true cart totals.
When an item is added to the cart, the cart total is found with the following on the Item:
addItem() {
inCart.push(this.props.id);
cartColors.push({ item: this.props.id, color: this.state.color, size: this.state.size });
cartTotal += (this.props.price);
this.setState({ show: false });
}
export let cartTotal = 0;
Then in the cart, I have the following:
import { inCart, cartTotal, cartColors } from '../PageItem/PageItem.js';
class Cart extends Component {
constructor(props) {
super(props);
this.state = {
cartList: inCart,
finalTotal: cartTotal.toFixed(2)
}
}
removeItem(itemId, itemPrice) {
var itemIndex = this.state.cartList.indexOf(itemId);
var newList = inCart.splice(itemIndex, 1);
var newTotal = (this.state.finalTotal - itemPrice).toFixed(2);
this.setState({ cartList: newList, finalTotal: newTotal });
}
The issue is - when I first go to the cart, it works. I see the true total based on the items currently in the cart, and when I remove the items the total updates correctly. However, when I leave the cart and add in more items, then navigate back into the cart, I see the previous cart total (the one shown when viewing the cart the first time).
I've tried updating the cartTotal within the removeItem function like so:
var newTotal = (cartTotal - itemPrice).toFixed(2);
And using
this.setState({ cartList: newList, finalTotal: newTotal });
But this provides a total that's off. Like it will add up correctly, but once I start to remove items it gets funky. It'll remove the item and update the price correctly the first time, but then on the second removal, the total reverts to the full previous total and removes the price from that - so it's off.
How can I get the total to update permanently?
Here are the full components:
PageItem component:
//Dependencies
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { Card, Button, Modal, Row, Container, Col } from 'react-bootstrap';
import PhotoSlider from '../../PhotoSlider/PhotoSlider.js';
import './PageItem.css';
import ColorButton from '../../ColorButton/ColorButton.js';
import SizeButton from '../../SizeButton/SizeButton.js';
class PageItem extends Component {
constructor(props, context) {
super(props, context);
this.handleShow = this.handleShow.bind(this);
this.handleClose = this.handleClose.bind(this);
this.addItem = this.addItem.bind(this);
this.changeColor = this.changeColor.bind(this);
this.changeSize = this.changeSize.bind(this);
this.state = {
show: false,
color: 1,
selectedColor: '',
size: 0
};
}
changeColor(colorId) {
this.setState({ color: colorId });
}
changeSize(sizeId) {
this.setState({ size: sizeId });
}
handleClose() {
this.setState({ show: false, color: 1 });
}
handleShow() {
this.setState({ show: true });
}
addItem() {
inCart.push(this.props.id);
cartColors.push({ item: this.props.id, color: this.state.color, size: this.state.size });
cartTotal += (this.props.price);
this.setState({ show: false });
}
render() {
return (
<div className="item">
<Card style={{ minWidth: '18rem' }} className="PageItem-Card" onClick={this.handleShow}>
<Card.Img className="PageItem-Card-Img" variant="top" src={this.props.img} />
<Card.Body className="PageItem-Card-Body">
<Card.Title className="PageItem-Title">{this.props.name}</Card.Title>
<Card.Text className="PageItem-Price">
{this.props.price}
</Card.Text>
<button className="PageItem-Button">Quick View</button>
</Card.Body>
</Card>
<Modal dialogClassName="custom-dialog" show={this.state.show} onHide={this.handleClose}>
<Modal.Header closeButton>
<Modal.Title>{this.props.name}</Modal.Title>
</Modal.Header>
<Modal.Body>
<Row>
<Col>
{this.props.colors.map((color) => {
if (color.colorId === this.state.color) {
return (
<div>
<PhotoSlider className="Modal-PhotoSlider"
img1={color.img1}
img2={color.img2}
img3={color.img3}
img4={color.img4}
img5={color.img5} />
</div>
)
}
})}
<div className="PageItem-ColorButton-Options">
{this.props.colors.map((color) => {
return (
<div className="PageItem-ColorButton">
<ColorButton
colorId={color.colorId}
colorName={color.colorName}
colorHex={color.colorHex}
colorImg={color.colorImg}
onClick={this.changeColor}
/>
</div>)
})}
</div>
{this.props.colors.map((color) => {
if (color.colorId === this.state.color) {
this.setState.selectedColor = color.colorName;
return (
<div>
<p>{color.colorName}</p>
</div>
)
}
})}
<div className="PageItem-SizeButton-Options">
{this.props.sizes.map((size) => {
return (
<div className="PageItem-SizeButton">
<SizeButton
sizeId={size.sizeId}
sizeValue={size.sizeValue}
onClick={this.changeSize}
/>
</div>)
})}
</div>
</Col>
<Col>
<p>{this.props.description}</p>
</Col>
</Row>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={this.handleClose}>
Close
</Button>
<Link to={`/products/${this.props.id}`}><Button variant="secondary">See More</Button></Link>
<Button variant="primary" onClick={this.addItem}>
Add to Cart
</Button>
</Modal.Footer>
</Modal>
</div>
);
}
}
export let inCart = [];
export let cartColors = [];
export let cartTotal = 0;
export default PageItem;
Cart component:
import React, { Component } from 'react';
import { Col, Row } from 'react-bootstrap';
import { inCart, cartTotal, cartColors } from '../PageItem/PageItem.js';
import CartItem from '../CartItem/CartItem.js';
import Products from '../../productData.js';
import Navbar from '../../Navbar/Navbar.js';
import Footer from '../../Footer/Footer.js';
import CartImg from '../../Images/SVG/cart2.svg';
class Cart extends Component {
constructor(props) {
super(props);
this.state = {
cartList: inCart,
finalTotal: cartTotal.toFixed(2)
}
}
removeItem(itemId, itemPrice) {
var itemIndex = this.state.cartList.indexOf(itemId);
var newList = inCart.splice(itemIndex, 1);
var newTotal = (this.state.finalTotal - itemPrice).toFixed(2);
this.setState({ cartList: newList, finalTotal: newTotal });
}
render() {
console.log(this.state.finalTotal);
return (
<div className="Page">
<Navbar />
<div className="Cart">
<img src={CartImg}></img>
<h1>Shopping Cart</h1>
<div className="Cart-Items">
{Products.map((product) => {
var productId = cartColors.find(item => item.item === product.id);
if (inCart.includes(product.id)) {
return (
<Row middle="xs" className="Cart-CartItem">
<Col xs={6}>
<CartItem
id={product.id}
name={product.name}
img={product.img}
description={product.description}
price={product.price}
/></Col>
<Col xs={6}>
<div className="Cart-CartOptions">
{product.colors.map((color) => {
if (productId.color === color.colorId) {
return (
<div>
<p>{color.colorName}</p>
</div>
)
}
})}
{product.sizes.map((size) => {
if (productId.size === size.sizeId) {
return (
<div>
<p>{size.sizeValue}</p>
</div>
)
}
})}
<button onClick={() => this.removeItem(product.id, product.price)}>Remove</button>
</div></Col>
</Row>
)
}
})}
</div>
<div className="Cart-Total">
<h1>{this.state.finalTotal}</h1>
</div>
</div>
<Footer />
</div>
)
}
}
export default Cart;

Each time you navigate away from the cart, your component likely unmounts (hard to know for sure without seeing the routes and how you render certain components), so when the component re-mounts, it re-initializes the finalTotal to the cartTotal variable which you import. Your removeItem function simply changes the Cart component's state variable, rather than the imported cartTotal variable, so when you navigate back to the cart, it will set the value to cartTotal which holds the old value of the variable, rather than what is changed by removeItem. Make sure to update this variable before setting it to the state using an updater function from the PageItem component or something similar:
import { inCart, cartTotal, updateTotal, cartColors } from '../PageItem/PageItem.js';
...
removeItem(itemId, itemPrice) {
var itemIndex = this.state.cartList.indexOf(itemId);
var newList = inCart.splice(itemIndex, 1);
var newTotal = (this.state.finalTotal - itemPrice).toFixed(2);
updateTotal(newTotal); // this will update the cartTotal variable in the PageItem component
this.setState({ cartList: newList, finalTotal: newTotal });
}
And in PageItem:
updateTotal(total) {
cartTotal = total; // so the total change will persist when you navigate away
}
Or, you can move your removeItem function to the PageItem component alongside the addItem function and mutate inCart directly and update cartTotal directly.
Also, Array.prototype.slice modifies an array in place, and returns an array of the deleted elements, so I'm not sure what you're trying to do with these lines:
var newList = inCart.splice(itemIndex, 1);
...
this.setState({ cartList: newList, finalTotal: newTotal });

Related

React.js: state is not updating in parent component

I have search filter and categories. I just want to have a possibility to reset state in single page application.
Due to React.js I guess I do everything correct to pass state from parent to child and then from child to parent. But, unfortunately, something is going wrong. I tried a lot and what I discovered, that onAddCategory() in DropdownGroup doesn't update current state.
Sorry in advance, I add whole code, my be something there could affect this. But i guess you can see first halfs of two codes and it will be enough.
Thank you in advance.
I have parent component:
class DropdownGroup extends React.Component {
constructor(props) {
super(props);
this.state = {
categories: [], // we have empty array, that pass to CategoryDropdown
};
this.onAddCategory = this.onAddCategory.bind(this);
}
onAddCategory(newCategory) {
this.setState(() => ({
categories: newCategory,
}));
}
onSelectCategory(path) {
this.props.onChangeEvents(path);
}
render() {
const months = ['January', 'February' ... ];
const eventsType = ['Party', 'Karaoke ... ];
const { categories } = this.state;
return (
<ButtonToolbar className="justify-content-center pb-4 pt-4">
{ console.log(categories) }
<CategoryDropdown
items={eventsType}
homePath="events"
path="events/categories/"
categories={categories} // here we pass our empty array (or updated later)
addCategories={this.onAddCategory} // this is what helps to update our array
onApply={(path) => this.onSelectCategory(path)}
/>
<MyDropdown
id="sort-by-month"
name="By month"
items={months}
onSelect={(e) => this.onSelectCategory(`events/month/${e}`)}
/>
<DropdownWithDate
oneDate="events/date/"
rangeDate="events/dates?from="
onApply={(path) => this.onSelectCategory(path)}
/>
<Button
onClick={() => this.setState({ categories: [] })} // here we can reset the value of our array
className="m-button ml-5"
>
Reset
</Button>
</ButtonToolbar>
);
}
}
DropdownGroup.propTypes = {
onChangeEvents: PropTypes.any.isRequired,
};
export default DropdownGroup;
and this is child component
class CategoryDropdown extends Component {
constructor(props) {
super(props);
this.state = {
visible: false,
selected: this.props.categories, // here we get values from props (now empty, then updated values)
};
this.onVisibleChange = this.onVisibleChange.bind(this);
}
onVisibleChange(visible) {
this.setState({
visible: visible,
});
}
saveSelected(selectedKeys) {
this.setState({
selected: selectedKeys,
});
}
addCategories() {
this.props.addCategories(this.state.selected); // here props are updated
}
confirm() {
const { selected } = this.state;
this.addCategories(this.state.selected);
const { homePath, path } = this.props;
if (selected.length > 0) {
this.props.onApply(path + selected);
} else {
this.props.onApply(homePath);
}
this.onVisibleChange(false);
}
render() {
const { visible } = this.state;
const { items } = this.props;
const menu = (
<Menu
multiple
onSelect={(e) => { this.saveSelected(e.selectedKeys); }}
onDeselect={(e) => { this.saveSelected(e.selectedKeys); }}
>
{items.map((item) => (
<MenuItem
key={item.replace('\u0020', '\u005f').toLowerCase()}
>
{item}
</MenuItem>
))}
<Divider />
<MenuItem disabled>
<Container
className="text-center "
style={{
cursor: 'pointer',
pointerEvents: 'visible',
}}
onClick={() => {
this.confirm();
}}
>
Select
</Container>
</MenuItem>
</Menu>
);
return (
<Dropdown
trigger={['click']}
onVisibleChange={this.onVisibleChange}
visible={visible}
closeOnSelect={false}
overlay={menu}
>
<Button className="m-button">By Category</Button>
</Dropdown>
);
}
}
CategoryDropdown.propTypes = {
onApply: PropTypes.any.isRequired,
items: PropTypes.any.isRequired,
path: PropTypes.string.isRequired,
homePath: PropTypes.string.isRequired,
categories: PropTypes.array.isRequired,
addCategories: PropTypes.any.isRequired,
};
export default CategoryDropdown;

React modal always take the last element of map function

I am currently creating a React todo application. So basically I have two component TodoList and TodoItem
TodoList component will receive an array of objects consisting title as the property and map through this with TodoItem component
In my TodoItem component, the user can choose to edit or delete the item. If the user chose to edit, a modal will show up with the existing title using a textarea. However, I have trouble implementing this function as the modal will always show up with the last element of the array.
TodoList Component
import React, { Component } from 'react'
import TodoItem from './TodoItem'
import { connect } from 'react-redux'
import { clear_todo } from '../store/actions/todoActions'
class Todolist extends Component {
clearList = (e) => {
e.preventDefault()
this.props.clearList(clear_todo());
}
handleChange = (index, title) => {
this.setState({
[index]: title
})
}
render() {
const { items, editItem } = this.props.todo
return (
<ul className="list-group my-5">
<h3 className="text-capitalize text-center">
Todo List
</h3>
{
items.map((item, index) => {
const editedTitle = item.title
return (<TodoItem key={item.id} title={item.title}
id={item.id}
editedTitle={editedTitle}
onChange={this.handleChange}
editItem={editItem} />)
})
}
<button className="btn btn-danger btn-block text-capitalize mt-5" onClick={this.clearList}> Clear List </button>
</ul>
)
}
}
const mapStateToProps = state => {
return {
todo: state.todo
}
}
const mapDispatchToProps = dispatch => {
return {
clearList: (clear_todo) => { dispatch(clear_todo) }
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Todolist)
TodoItem Component
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { delete_todo, edit_todo, toggle_edit } from '../store/actions/todoActions'
import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Form, FormGroup, Input } from 'reactstrap';
import TodoEditItem from './TodoEditItem'
class Todoitem extends Component {
// constructor(props) {
// super(props)
// this.state = {
// [props.id]: props.title
// }
// }
handleEdit = (id, title) => {
this.props.editTodo(edit_todo(id, title))
}
toggleEdit = (editItem, title) => {
this.props.toggleEdit(toggle_edit(!editItem))
// this.initializeTitle(title)
}
handleDelete = (id) => {
this.props.deleteTodo(delete_todo(id))
}
// onChange = (e, id) => {
// this.setState({
// [id]: e.target.value
// })
// }
componentDidMount() {
// console.log(this.props)
// this.initializeTitle(this.props.title)
this.setState({
[this.props.id]: this.props.editedTitle
})
console.log(this.state)
}
render() {
// console.log(this.state)
let { id, title, editItem, editedTitle, index } = this.props
console.log(id)
// console.log(index)
// let { item } = this.state
return (
<div>
<li className="list-group-item text-capitlize d-flex justify-content-between my-2">
<h6>{title}</h6>
<div className="todo-icon">
<span className="mx-2 text-success" onClick={this.toggleEdit.bind(this, editItem)} >
<i className="fas fa-pen"></i>
</span>
<span className="mx-2 text-danger" onClick={this.handleDelete.bind(this, id)}>
<i className="fas fa-trash"></i>
</span>
</div>
<Modal isOpen={editItem}>
<ModalHeader>Edit Todo Item</ModalHeader>
<ModalBody>
<Form>
<FormGroup row>
<Input type="textarea" name="text" value={this.state ? this.state[id] : ""} onChange={this.props.onChange} />
</FormGroup>
</Form>
</ModalBody>
<ModalFooter>
<Button color="primary" onClick={this.handleEdit.bind(this, id, editedTitle)}>Save</Button>{' '}
<Button color="secondary" onClick={this.toggleEdit.bind(this, editItem)}> Cancel</Button>
</ModalFooter>
</Modal>
</li>
{/* {editItem ? <TodoEditItem title={title} editItem={editItem} /> : ''} */}
</div>
)
}
}
const mapDispatchToProps = dispatch => {
return {
deleteTodo: (delete_todo) => { dispatch(delete_todo) },
editTodo: (edit_todo) => { dispatch(edit_todo) },
toggleEdit: (toggle_edit) => { dispatch(toggle_edit) },
}
}
export default connect(null, mapDispatchToProps)(Todoitem)
Sample Image
Image of TodoItem
Image of Modal
As you can see from the image of TodoItem, I am trying to edit "Take out the garbage" but my textarea has been prepopulated as the last element.
You're using the same variable to determine all of the TodoItem's open state. The result is it appears that it is only taking the last value from the array, but really each modal is opening at once and the last one is the only visible modal.
// editItem here is used to determine the open state of both modals
const { items, editItem } = this.props.todo
...
{
items.map((item, index) => {
const editedTitle = item.title
return (
<TodoItem
key={item.id}
title={item.title}
id={item.id}
editedTitle={editedTitle}
onChange={this.handleChange}
editItem={editItem} // Same value
/>
)
})
}
...
let { editItem } = this.props
<Modal isOpen={editItem}> // All will open and close at the same time
Instead, use a different flag, or just manage the open state in each child like this:
<span className="mx-2 text-success" onClick={() => this.setState({open: true})} >
<i className="fas fa-pen"></i>
</span>
...
<Modal isOpen={this.state.open}>

How can i replace thumbnail by video on click and trigger playing on mobile?

I know several topics exist about this subject but i don't find the "real" answer....
First of all, i work on React JS and i'm a beginner ;)
With the YouTube API, i mapped the items in the JSON object to display video thumbnails in cards. On desktop mode, when i click on thumbnail, it trigger a modal with the video.
But on mobile, i have 2 issues.....
I want, in the same time, replace (maybe replaceWith and data-video can be helpfull...) the thumbnail by the video who match with, and i want to trigger autoplay and fullscreen.....i don't know if the second part is possible....Despite my research, i still don't if i can get around this mobile block....
Thanks a lot for the answers and the advices :)
Here is my code below :
import React, { Component } from 'react'
import axios from 'axios';
import posed from 'react-pose';
import ReactPlayer from 'react-player';
import { Card, CardImg, Col, Row, CardTitle, Modal, ModalBody, ModalHeader, Pagination, PaginationItem, PaginationLink } from 'reactstrap';
import './videos.css'
const Box = posed.div({
hidden: { opacity: 0 },
visible: { opacity: 1 }
});
class Videos extends Component {
constructor () {
super();
this.state = {
isVisible : true,
videos: [],
resultsDatas: "",
nextPage: "",
previousPage: "",
videoId: "",
search: "",
playing: false,
modal : false,
clickedVideoId: "",
clickedVideoTitle: ""
}
}
componentDidMount() {
axios.get('https://www.googleapis.com/youtube/v3/playlistItems?key=......API KEY........&playlistId=PL3-4jitn5YqtnR_mrYoz_Qb0z7frXZcW6&type=video&showinfo=0&iv_load_policy=3&part=snippet,id&order=date&maxResults=24')
.then((result) => {
this.setState({
videos: result.data.items,
resultsDatas: result.data,
nextPage: result.data.nextPageToken});
console.log(this.state.videos);
}
)
setTimeout(() => {this.setState({ isVisible: !this.state.isVisible });
}, 400);
}
nextPage = () => {
axios.get(`https://www.googleapis.com/youtube/v3/playlistItems?key=......API KEY........&playlistId=PL3-4jitn5YqtnR_mrYoz_Qb0z7frXZcW6&type=video&part=snippet,id&order=date&maxResults=24&pageToken=${this.state.nextPage}`)
.then((result) => {
this.setState({
nextPage: result.data.nextPageToken,
previousPage: result.data.prevPageToken,
videos: result.data.items,
resultsDatas: result.data
});
})
}
previousPage = () => {
axios.get(`https://www.googleapis.com/youtube/v3/playlistItems?key=......API KEY........&playlistId=PL3-4jitn5YqtnR_mrYoz_Qb0z7frXZcW6&type=video&part=snippet,id&order=date&maxResults=24&pageToken=${this.state.previousPage}`)
.then((result) => {
this.setState({
nextPage: result.data.nextPageToken,
previousPage: result.data.prevPageToken,
videos: result.data.items,
resultsDatas: result.data});
})
}
//For search function
inputChange = (event) => {
this.setState({search: event.target.value })
}
playPause = (index) => {
let element = document.getElementById(`play-pause${index}`);
if (element.requestFullscreen) {
this.setState({playing: !this.state.playing})
}
}
toggleModal = () => {
this.setState({
modal: !this.state.modal
});
}
onThumbnailClick = (index) => {
this.setState({
clickedVideoId: this.state.videos[index].snippet.resourceId.videoId,
clickedVideoTitle: this.state.videos[index].snippet.title
})
var windowWidth = window.innerWidth;
if(windowWidth > 768){
this.toggleModal()
}
}
render() {
let mapedVideos = this.state.videos.filter(
(item) => {
return item.snippet.title.toLowerCase().indexOf(this.state.search) !==-1;
}
);
return (
<div>
{/* <Row className="search-pages-bar pt-3 d-flex justify-content-center">
<Pagination>
<PaginationItem onClick={this.previousPage}>
<PaginationLink previous href="#"/>
</PaginationItem>
<PaginationItem onClick={this.nextPage}>
<PaginationLink next href="#"/>
</PaginationItem>
</Pagination>
</Row> */}
<Box pose={this.state.isVisible ? 'hidden' : 'visible'}>
<Row className="custom-videos-row mb-5">
{mapedVideos.map((item, index) => {
this.state.videos.map((item) => {
if(item.snippet.title.length > 38) {
return (item.snippet.title=`${item.snippet.title.slice(0,37)}... `);
}
})
return (
<Col className="custom-videos-col" lg="3" md="4" sm="6" key={index}>
<Card className="card-wrapper" onClick={() => {this.onThumbnailClick(index)}}>
<CardImg className="video-thumbnail" src={item.snippet.thumbnails.medium.url} alt="thumb"></CardImg>
<CardTitle className="video-card-title pl-2"><img className="play-picto" src={process.env.PUBLIC_URL + '/images/play_L.png'} alt="play-picto"/>{item.snippet.title}
</CardTitle>
</Card>
</Col>
);
})}
<Modal className="video-modal" role="dialog" isOpen={this.state.modal} toggle={this.toggleModal}>
<ModalHeader className="custom-modal-header" toggle={this.toggleModal}>
<img src={process.env.PUBLIC_URL + './images/logo.png'} alt="La Baule TV logo" className="LBTV-logo"></img>
<span className="ml-2">{this.state.clickedVideoTitle}</span></ModalHeader>
<ModalBody>
<ReactPlayer className="video-player"
url={`https://www.youtube.com/watch?v=${this.state.clickedVideoId}`}
playing= {true}
config={{
youtube: {
playerVars: {
modestbranding: 1,
controls: 1,
rel: 0
}
}}}
/>
</ModalBody>
</Modal>
</Row>
</Box>
</div>
)
}
}
export default Videos;
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>

Bootstrap dropdowns in ReactJS, only one open at a time

I have a page contains multiple Bootstrap Cards and each card is a component and each card footer is also a component. Card Footer contains buttons. When you click on a button, drop down will be opened like below
At any point of time when I click on a button, other drop downs should be in closed state. But its happening like this...
Requirement: One more thing is when I click on the same button, the respective drop down should be closed.
Requirement: When I click on any item inside drop down the respective drop down should be closed
My Architecture is like below
HOME PAGE COMPONENT CODE -START
class HomePage extends React.Component {
constructor(props) {
super(props);
this.state = {
items: [],
activatedIdStoredInParent: ""
};
}
toggleCountersMenu = (name) => {
var name1 = name;
this.setState((prevState) => {
return {
activatedIdStoredInParent: name1
}
});
}
render() {
const products = this.state.items.map((item, index) => {
return <div>
<Card
product={item}
activatedIdStoredInParent={this.state.activatedIdStoredInParent}
toggleCountersMenu={this.toggleCountersMenu}
>
</Card>;
</div>
});
return (
<div>
<div className="card-columns">
{products}
</div>
</div >
);
}
}
export default HomePage;
HOME PAGE COMPONENT CODE - END
CARD COMPONENT CODE - START
class Card extends React.Component {
handleActionClick = (name) => {
this.props.toggleCountersMenu(name);
}
render() {
return (
<div key={this.props.product.name}>
<CardHeader product={this.props.product} />
<CardBody product={this.props.product} />
<CardFooter
product={this.props.product}
onActionItemClick={this.handleActionClick}
activatedIdStoredInParent={this.props.activatedIdStoredInParent}
/>
</div>
);
}
}
export default Card;
CARD FOOTER COMPONENT CODE - START
class CardFooter extends React.Component {
handleActionItemClick = (name) => {
this.props.onActionItemClick(name);
}
render() {
console.log('Card Footer Drop Down comp rendered');
return (
<div className=" card-footer text-center">
<ButtonDropdown text="F" className="danger"
product={this.props.product}
onActionItemClick={this.handleActionItemClick}
activatedIdStoredInParent={this.props.activatedIdStoredInParent}
></ButtonDropdown>
</div>
);
}
}
export default CardFooter;
ButtonDropdown COMPONENT CODE - START
class ButtonDropdown extends React.Component {
constructor(props) {
super(props);
this.state = {
open: false,
show: ' none',
localActivatedId: 'none'
}
}
toggleOpen = (e) => {
var name = e.target.name;
this.setState((prevState, props) => {
var item = {
localActivatedId: name
}
if (props.activatedIdStoredInParent === name) {
if (prevState.show === ' show') {
item.show = ' none';
}
else {
item.show = ' show';
}
}
return item;
});
this.props.onActionItemClick(name);
}
numberClick = (e) => {
var qty = e.target.innerText;
this.setState((prevState, props) => {
var item = {
show: ' none'
}
return item;
});
}
render() {
return (
<div className="btn-group" >
<button type="button" className={`btn btn-${this.props.className} mr-1`} name={this.props.product.name + '$$' + this.props.text} onClick={this.toggleOpen}>
{this.props.text} (classAdded={this.state.show})
</button>
<div className={`dropdown-menu ${this.state.show}`}>
<span className="dropdown-item cursor-pointer " onClick={this.numberClick}>
-1
</span>
<span className="dropdown-item cursor-pointer" onClick={this.numberClick}>
-2
</span>
</div>
</div>
);
}
}
export default ButtonDropdown;
When I add multiple buttonDropdown components in Card Footer the end product is like this. How can I close other dropdowns.
I would like to know is my architecture is correct.. I am not using Redux/Flux etc..
You can use the componentDidUpdate lifecycle, in order to update your state's property that is opening the dropdown.
I don't know if it's the open or show property that displays the content of the dropdown but here's my logic.
class ButtonDropdown extends React.Component {
constructor(props) {
super(props);
this.state = {
//
};
}
componentDidUpdate(prevProps) {
const name = this.props.product.name + '$$' + this.props.text;
if (prevProps.activatedIdStoredInParent !== this.props.activatedIdStoredInParent && this.props.activatedIdStoredInParent !== name) {
this.closeDropDown();
}
}
closeDropDown = () => this.setState({ isOpen: false });
toggleOpen = (e) => {
//
}
numberClick = (e) => {
//
}
render() {
//
}
}
export default ButtonDropdown;

How to use react-virtualized with dynamic list height

I am trying to implement react-virtualized into my project. I have a side panel with a list of subdivisions. I would like to have an accordion-like functionality when the user selects an item. When the page first loads it looks like it is working correctly.
However when I start to scroll down the list looks like this
and here is code
const mapStateToProps = state => ({
windowHeight: state.dimensions.windowHeight,
sbds: new Immutable.List(state.sbds.data),
sbd: state.sbd.data
})
#connect(mapStateToProps)
export default class SBD extends Component {
static propTypes = {
windowHeight: PropTypes.number,
sbds: PropTypes.instanceOf(Immutable.List),
sbd: PropTypes.object
}
constructor(props) {
super(props)
this.state = {
listHeight: props.windowHeight - 250,
listRowHeight: 60,
overscanRowCount: 10,
rowCount: props.sbds.size,
scrollToIndex: undefined,
collapse: true
}
}
componentWillUnmount() {
}
shouldComponentUpdate(nextProps, nextState) {
const {sbds} = this.props
if (shallowCompare(this, nextProps, nextState))
return true
else return stringify(sbds) !== stringify(nextProps.sbds)
}
_handleSelectRow = selected => {
sbdRequest(selected)
const obj = {[selected.id]: true}
this.setState(obj)
}
render() {
const {
listHeight,
listRowHeight,
overscanRowCount,
rowCount,
scrollToIndex
} = this.state
return (
<div>
<SearchGroup />
<Card className='border-0 mt-10 mb-0'>
<CardBlock className='p-0'>
<AutoSizer disableHeight>
{({width}) => (
<List
ref='List'
className=''
height={listHeight}
overscanRowCount={overscanRowCount}
rowCount={rowCount}
rowHeight={listRowHeight}
rowRenderer={this._rowRenderer}
scrollToIndex={scrollToIndex}
width={width}
/>
)}
</AutoSizer>
</CardBlock>
</Card>
</div>
)
}
_getDatum = index => {
const {sbds} = this.props
return sbds.get(index % sbds.size)
}
_rowRenderer = ({index}) => {
const {sbd} = this.props
const datum = this._getDatum(index)
return (
<span key={datum.id}>
<Button
type='button'
color='link'
block
onClick={() => this._handleSelectRow(datum)}>
{datum.name}
</Button>
<Collapse isOpen={this.state[datum.id]}>
<Card>
<CardBlock>
FOO BAR
</CardBlock>
</Card>
</Collapse>
</span>
)
}
}
You're not setting the style parameter (passed to your rowRenderer). This is the thing that absolutely positions rows. Without it, they'll all stack up in the top/left as you scroll.
https://bvaughn.github.io/forward-js-2017/#/32/1

Resources