How to use react-virtualized with dynamic list height - reactjs

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

Related

React State Reverts to Previous When Navigating Away

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 });

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;

Use Vertical Steps component from antd with in a ScrollPanel from PrimeReact

I would like to use the Vertical Steps from antd(https://ant.design/components/steps/) within a primeReact ScrollPanel(https://www.primefaces.org/primereact/#/scrollpanel). I would also like to use the "Next" and "Previous" button as shown in the "antd" example. All works fine till here, but when I have a lot of steps and the click on "Next" moves to next step but the Scroller doesn't move i.e. the highlighted step moves out of View. How to make the scroll also move so the selected Step is in centre View?
Code for vertical Steps inside ScrollPanel:
class VerticalStepsWithContent extends Component {
constructor(props) {
super(props);
this.state = { current: 0 };
}
next() {
const current = this.state.current + 1;
this.setState({ current });
}
prev() {
const current = this.state.current - 1;
this.setState({ current });
}
onChange = current => {
console.log("onChange:", current);
this.setState({ current });
};
render() {
const { Step } = Steps;
const steps = [
{
title: "First",
content: "First-content"
},
{
title: "Second",
content: "Second-content"
},
{
title: "Last",
content: "Last-content"
}
];
const { current } = this.state;
return (
<div>
<div className="steps-action">
{current < steps.length - 1 && (
<Button type="primary" onClick={() => this.next()}>
Next
</Button>
)}
{current === steps.length - 1 && (
<Button
type="primary"
onClick={() => message.success("Processing complete!")}
>
Done
</Button>
)}
{current > 0 && (
<Button style={{ marginLeft: 8 }} onClick={() => this.prev()}>
Previous
</Button>
)}
</div>
<ScrollPanel
header="Summary"
style={{ height: "400px" }}
className="custom"
>
<Steps
direction="vertical"
current={current}
onChange={this.onChange}
>
{steps.map(item => (
<Step key={item.title} title={item.title} />
))}
</Steps>
</ScrollPanel>
{/* <div className="steps-content">{steps[current].content}</div> */}
</div>
);
}
}
export default VerticalStepsWithContent;
You didn't bind class functions to this instance (which is a common bug):
class VerticalStepsWithContent extends Component {
constructor(props) {
super(props);
this.state = { current: 0 };
this.next = this.next.bind(this);
// bind rest of the functions
}
}
You should avoid the use of constructor, instead use class properties:
class VerticalStepsWithContent extends Component {
state = { current: 0 };
next = () => {
const current = this.state.current + 1;
this.setState({ current });
};
// ...
}
export default VerticalStepsWithContent;

ReactJS onClick event on an array element

I need to select images in an image grid. Currently, I can only select one image at a time. When selecting one, the other unselects. I'd like to select every image I want (many images selected at a time). I can't activate a isToggle boolean on each element with onClick event.
class Gallery extends Component{
constructor(props) {
super(props);
this.state = {
//isToggleOn: true,
selected: '',
};
this.handleClick = this.handleClick.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleClick = key => {
this.setState(state => ({
//isToggleOn: !state.isToggleOn,
selected: key,
}));
//console.log(this.state)
}
render(){
//const classes = useStyles();
return (
<Container>
<Row >
<Col>
<div style={styles.root}>
<GridList cellHeight={160} style={styles.gridList} cols={3}>
{tileData.map((tile,i) => (
<GridListTile key={i} cols={tile.cols || 1} onClick={() => this.handleClick(tile)} >
<img src={tile.img} alt={tile.title} style={this.state.selected === tile ? styles.inputClicked:styles.inputNormal} />
</GridListTile>
))}
</GridList>
</div>
</Col>
</Row>
</Container>
);
}
}
export default Gallery;
I can only select one image at a time. I expect to select many at a time.
When the tile gets clicked, you need to add to an array of selected tiles like this:
handleClick = tile => {
this.setState({
selected: this.state.selected.concat([tile]),
});
}
then when rendering, you can check if the tile is selected something like this:
<img src={tile.img} alt={tile.title} style={isSelected(this.state.selected, tile) ? styles.inputClicked:styles.inputNormal} />
with:
isSelected = (selected, tile) => {
// some logic to check that the tile is already selected e.g.
return selected.find(tileItem => tileItem.title === tile.title)
}
Ensure to initialise your selected state to [] in constructor :
this.state = {
selected: [],
};
UPDATE:
If you want to remove tiles too, you'd have to do something like this:
handleClick = tile => {
const { selected } = this.state;
if(selected.find(item ==> item.title === tile.title)) {
// need to remove tile
this.setState({
selected: selected.filter(item => item.tile !== tile.title),
});
} else {
this.setState({
selected: this.state.selected.concat([tile]),
});
}
}

Callback function, responsible for updating state, passed as props to child component not triggering a state update

The callback function (lies in Images component) is responsible for making a state update. I'm passing that function as props to the Modal component, and within it it's being passed into the ModalPanel component.
That function is used to set the state property, display, to false which will close the modal. Currently, that function is not working as intended.
Image Component:
class Images extends Component {
state = {
display: false,
activeIndex: 0
};
handleModalDisplay = activeIndex => {
this.setState(() => {
return {
activeIndex,
display: true
};
});
};
closeModal = () => {
this.setState(() => {
return { display: false };
});
}
render() {
const { imageData, width } = this.props;
return (
<div>
{imageData.resources.map((image, index) => (
<a
key={index}
onClick={() => this.handleModalDisplay(index)}
>
<Modal
closeModal={this.closeModal}
display={this.state.display}
activeIndex={this.state.activeIndex}
selectedIndex={index}
>
<Image
cloudName={CLOUDINARY.CLOUDNAME}
publicId={image.public_id}
width={width}
crop={CLOUDINARY.CROP_TYPE}
/>
</Modal>
</a>
))}
</div>
);
}
}
export default Images;
Modal Component:
const overlayStyle = {
position: 'fixed',
zIndex: '1',
paddingTop: '100px',
left: '0',
top: '0',
width: '100%',
height: '100%',
overflow: 'auto',
backgroundColor: 'rgba(0,0,0,0.9)'
};
const button = {
borderRadius: '5px',
backgroundColor: '#FFF',
zIndex: '10'
};
class ModalPanel extends Component {
render() {
const { display } = this.props;
console.log(display)
const overlay = (
<div style={overlayStyle}>
<button style={button} onClick={this.props.closeModal}>
X
</button>
</div>
);
return <div>{display ? overlay : null}</div>;
}
}
class Modal extends Component {
render() {
const {
activeIndex,
children,
selectedIndex,
display,
closeModal
} = this.props;
let modalPanel = null;
if (activeIndex === selectedIndex) {
modalPanel = (
<ModalPanel display={this.props.display} closeModal={this.props.closeModal} />
);
}
return (
<div>
{modalPanel}
{children}
</div>
);
}
}
export default Modal;
links to code
https://github.com/philmein23/chez_portfolio/blob/chez_portfolio/components/Images.js
https://github.com/philmein23/chez_portfolio/blob/chez_portfolio/components/Modal.js
You're dealing with this modal through a very non-react and hacky way.
Essentially, in your approach, all the modals are always there, and when you click on image, ALL modals display state becomes true, and you match the index number to decide which content to show.
I suspect it's not working due to the multiple children of same key in Modal or Modal Panel.
I strongly suggest you to ditch current approach. Here's my suggestions:
Only a single <Modal/> in Images component.
Add selectedImage state to your Images component. Every time you click on an image, you set selectedImage to that clicked image object.
Pass selectedImage down to Modal to display the content you want.
This way, there is only ONE modal rendered at all time. The content changes dynamically depending on what image you click.
This is the working code I tweaked from your repo:
(I'm not sure what to display as Modal content so I display public_id of image)
Images Component
class Images extends Component {
state = {
display: false,
selectedImage: null
};
handleModalDisplay = selectedImage => {
this.setState({
selectedImage,
display: true
})
};
closeModal = () => {
//shorter way of writing setState
this.setState({display: false})
}
render() {
const { imageData, width } = this.props;
return (
<div>
<Modal
closeModal={this.closeModal}
display={this.state.display}
selectedImage={this.state.selectedImage}
/>
{imageData.resources.map((image, index) => (
<a
//Only use index as key as last resort
key={ image.public_id }
onClick={() => this.handleModalDisplay(image)}
>
<Image
cloudName={CLOUDINARY.CLOUDNAME}
publicId={image.public_id}
width={width}
crop={CLOUDINARY.CROP_TYPE}
/>
</a>
))}
</div>
);
}
}
Modal Component
class Modal extends Component {
render() {
const { display, closeModal, selectedImage } = this.props;
const overlayContent = () => {
if (!selectedImage) return null; //for when no image is selected
return (
//Here you dynamically display the content of modal using selectedImage
<h1 style={{color: 'white'}}>{selectedImage.public_id}</h1>
)
}
const overlay = (
<div style={overlayStyle}>
<button style={button} onClick={this.props.closeModal}>
X
</button>
{
//Show Modal Content
overlayContent()
}
</div>
);
return <div>{display ? overlay : null}</div>;
}
}

Resources