Retain scroll position on back in React js - javascript

I am trying to retain the scroll position on back button but the scroll position is going back to the 0,0
The code is as follows -
<SearchListWrapper className="TestSL" ref={this.myRef} onScroll={this.handleScrollPosition} >
handleScrollPosition(e){
sessionStorage.setItem("scrollPosition", this.myRef.current!.scrollTop.toString());
};
async componentDidMount() {
console.log("inside CDM")
if(sessionStorage.getItem("scrollPosition"))
{
const scrollpos =Number(sessionStorage.getItem("scrollPosition"))
this.myRef.current!.scrollTo(0,scrollpos)
}
}
I tried the above code and expecting the values in session to be fixed and not change back to 0,0 on back button.

Here's a solution for you:
example-child-scroll.jsx
// This example will manage the scroll position for a DOM element.
// The key must be unique per managed element, as it is used as a key in a store
// when saving and restoring the position.
import React from 'react'
import ScrollManager from './ScrollManager'
export default function App() {
return (
<div>
<ScrollManager scrollKey="some-list-key">
{({ connectScrollTarget, ...props }) =>
<div ref={connectScrollTarget} style={{ overflow: 'auto', maxHeight: 500 }}>
If this div is unmounted and remounted, scroll position is restored.
</div>
}
</ScrollManager>
</div>
)
}
ScrollManager.jsx
import React from 'react'
import requestAnimationFrame from 'raf'
export const memoryStore = {
_data: new Map(),
get(key) {
if (!key) {
return null
}
return this._data.get(key) || null
},
set(key, data) {
if (!key) {
return
}
return this._data.set(key, data)
}
}
/**
* Component that will save and restore Window scroll position.
*/
export default class ScrollPositionManager extends React.Component {
constructor(props) {
super(...arguments)
this.connectScrollTarget = this.connectScrollTarget.bind(this)
this._target = window
}
connectScrollTarget(node) {
this._target = node
}
restoreScrollPosition(pos) {
pos = pos || this.props.scrollStore.get(this.props.scrollKey)
if (this._target && pos) {
requestAnimationFrame(() => {
scroll(this._target, pos.x, pos.y)
})
}
}
saveScrollPosition(key) {
if (this._target) {
const pos = getScrollPosition(this._target)
key = key || this.props.scrollKey
this.props.scrollStore.set(key, pos)
}
}
componentDidMount() {
this.restoreScrollPosition()
}
shouldComponentUpdate(nextProps, nextState, nextContext) {
if (this.props.scrollKey !== nextProps.scrollKey) {
this.saveScrollPosition()
}
return true;
}
componentDidUpdate(prevProps) {
if (this.props.scrollKey !== prevProps.scrollKey) {
this.restoreScrollPosition()
}
}
componentWillUnmount() {
this.saveScrollPosition()
}
render() {
const { children = null, ...props } = this.props
return (
children &&
children({ ...props, connectScrollTarget: this.connectScrollTarget })
)
}
}
ScrollPositionManager.defaultProps = {
scrollStore: memoryStore
}
function scroll(target, x, y) {
if (target instanceof window.Window) {
target.scrollTo(x, y)
} else {
target.scrollLeft = x
target.scrollTop = y
}
}
function getScrollPosition(target) {
if (target instanceof window.Window) {
return { x: target.scrollX, y: target.scrollY }
}
return { x: target.scrollLeft, y: target.scrollTop }
}

Related

Error: ReactGridLayout: ReactGridLayout.children[0].x must be a number

I'm trying to make a RGL layout where I can add and remove tiles and save the configuration. I need the file to be a function component and not a class component.
Here's what I've got so far:
import React from "react";
import { Responsive, WidthProvider } from "react-grid-layout";
import { v4 as uuid } from "uuid";
const ReactGridLayout = WidthProvider(Responsive);
localStorage.setItem("username");
const username = localStorage.getItem('username');
/**
* This layout demonstrates how to sync multiple responsive layouts to localstorage.
*/
export default class Dash extends React.PureComponent {
constructor(props) {
super(props);
this.originalLayouts = this.getLayout("layouts") || {lg:[]};
this.savedLayout = JSON.parse(JSON.stringify(this.originalLayouts));
this.state = {
layout: this.savedLayout.lg.map(function(i, key) {
return {
i: "",
x: i * 2,
y: 0,
w: 2,
h: 2
};
}
),
};
this.id = uuid()
this.onLayoutChange = this.onLayoutChange.bind(this);
this.addItem = this.addItem.bind(this);
this.removeItem = this.removeItem.bind(this);
this.now = this.now.bind(this);
this.saveLayout = this.saveLayout.bind(this);
this.getLayout = this.getLayout.bind(this);
}
// Function to create element on the Dashboard
createElement(el, i) {
const removeStyle = {
position: "absolute",
right: "2px",
top: 0,
cursor: "pointer"
};
return (
<div key={el.i} data-grid={i}>
<span
className="remove"
style={removeStyle}
onClick={this.removeItem.bind(this, el.i)}
>
x
</span>
</div>
);
}
// Function ammends layout array with additional grid item
addItem = (item) => {
const id = uuid();
console.log('adding: ' + id);
this.setState({
// Add a new item. It must have a unique key!
layout: this.state.layout.concat({
i: `${id}`,
x: (this.state.layout.length * 2) % (this.state.cols || 12),
y: 0, // puts it at the bottom
w: 2,
h: 2
})
},
}
// Function to remove grid item on click
removeItem = (i) => {
console.log('removing: ' + i);
this.setState({layout: _.reject(this.state.layout, { i:i }) });
}
// Function when the layout change
onLayoutChange= (layout, layouts) => {
this.saveLayout("layouts", layouts);
this.setState({layout: layout});
}
// Function to save user layout to LS
saveLayout = ( key, value ) => {
if (global.localStorage) {
global.localStorage.setItem(username, JSON.stringify({ [key]: value}) );
console.log("Saving Layout...", value);
}
};
// Function to get user layout from LS
getLayout = (key) => {
let ls = {};
if (global.localStorage) {
try {
ls = JSON.parse(global.localStorage.getItem(username)) || {};
} catch (error) {
console.log('error getting layout')
}
}
return ls[key]
};
render() {
return (
<div id="maincontent" className="content">
<ReactGridLayout
className="layout"
resizeHandles={['se']}
compactType={"vertical"}
draggableHandle=".dragHandle"
onLayoutChange={this.onLayoutChange}
// onBreakpointChange={this.onBreakpointChange}
{...this.props}
>
{_.map(this.state.layout, (elem, i) => this.createElement(elem, i))}
</ReactGridLayout>
</div>
);
}
}
Can you spot where I made a mistake ? I can't find what is the problem. I can start with a blank dashboard, then add an item and it works.
But if I add a second item ! This error is raised.
Thanks !

Assigning ScrollTo value cause unexpected flickering/blinking on iOS devices

We recently worked on an auto-scrolling while freely swipeable component using React.js. The implementation idea is inspired by this article
And we've made something like this in React:
import React, { Component } from "react";
import PropTypes from "prop-types";
import "./AutoScroller.css";
const NUM_OF_CLONES = 10;
const AUTO_SCROLL_OFFSET = 1; // min offset of scrollTo is 1
const AUTO_SCROLL_INTERVAL = 32; // 1000 ms / 30 fps
export default class AutoScroller extends Component {
static propTypes = {
contents: PropTypes.array.isRequired,
itemWidth: PropTypes.number.isRequired,
numsOfItemsPerScreen: PropTypes.number.isRequired
};
constructor(props) {
super(props);
this.autoScrollerRef = React.createRef();
this.currentPosition = 0;
this.autoScrollTimer = null;
this.scrollingTimer = null;
/* boolean status */
this.isTouch = false;
this.isScrolling = false;
}
componentDidMount() {
this.startAutoScroll();
this.autoScrollerRef.current.addEventListener(
"touchstart",
this.touchStartHandler
);
this.autoScrollerRef.current.addEventListener(
"touchend",
this.touchEndHandler
);
this.autoScrollerRef.current.addEventListener("scroll", this.scrollHandler);
this.autoScrollerRef.current.addEventListener(
"contextmenu",
this.contextMenuHandler
);
}
componentWillUnmount() {
this.clearAutoScroll();
this.clearScrollingTimer();
this.autoScrollerRef.current.removeEventListener(
"touchstart",
this.touchStartHandler
);
this.autoScrollerRef.current.removeEventListener(
"touchend",
this.touchEndHandler
);
this.autoScrollerRef.current.removeEventListener(
"scroll",
this.scrollHandler
);
this.autoScrollerRef.current.removeEventListener(
"contextmenu",
this.contextMenuHandler
);
}
touchStartHandler = () => {
this.isTouch = true;
this.clearAutoScroll();
};
touchEndHandler = () => {
this.isTouch = false;
if (!this.isScrolling) {
this.currentPosition = this.autoScrollerRef.current.scrollLeft;
this.startAutoScroll();
}
};
scrollHandler = () => {
const {
contents: { length },
itemWidth
} = this.props;
this.isScrolling = true;
this.currentPosition = this.autoScrollerRef.current.scrollLeft;
const maxOffset = length * itemWidth;
if (this.currentPosition > maxOffset) {
const offset = this.currentPosition - maxOffset;
this.autoScrollerRef.current.scrollTo(offset, 0);
this.currentPosition = offset;
} else if (this.currentPosition <= 0) {
const offset = this.currentPosition + maxOffset;
this.autoScrollerRef.current.scrollTo(offset, 0);
this.currentPosition = offset;
}
/***
* note: there will be only one timer, and the timer is only created by the very last scroll
* only when the scroll event is not triggered anymore, the timer starts to get executed.
*/
if (this.scrollingTimer) {
clearTimeout(this.scrollingTimer);
}
this.scrollingTimer = setTimeout(() => {
this.isScrolling = false;
/***
* note: resume auto-scroll when the momentum scroll (after finger leaves) stalls the scroll
*/
if (!this.isTouch) {
this.startAutoScroll();
}
}, 300);
};
contextMenuHandler = (event) => {
event.preventDefault();
};
startAutoScroll = () => {
if (!this.autoScrollTimer) {
this.autoScrollTimer = setInterval(this.autoScroll, AUTO_SCROLL_INTERVAL);
}
};
clearAutoScroll = () => {
if (this.autoScrollTimer) {
clearInterval(this.autoScrollTimer);
this.autoScrollTimer = null;
}
};
clearScrollingTimer = () => {
if (this.scrollingTimer) {
clearTimeout(this.scrollingTimer);
this.scrollingTimer = null;
}
};
autoScroll = () => {
const {
contents: { length },
itemWidth,
numsOfItemsPerScreen
} = this.props;
if (this.currentPosition < 0) {
this.currentPosition = 0;
}
if (length > numsOfItemsPerScreen) {
const position = this.currentPosition + AUTO_SCROLL_OFFSET;
this.autoScrollerRef.current.scrollTo(position, 0);
const maxOffset = length * itemWidth;
if (this.currentPosition > maxOffset) {
const offset = this.currentPosition - maxOffset;
this.autoScrollerRef.current.scrollTo(offset, 0);
this.currentPosition = offset;
} else {
this.currentPosition = position;
}
}
};
getWrappedData = () => {
const { contents } = this.props;
const { length } = contents;
const numberOfClones = length < NUM_OF_CLONES ? length : NUM_OF_CLONES;
return [...contents, ...contents.slice(0, numberOfClones)];
};
render() {
const { itemGap, lineHeight } = this.props;
return (
<div className="auto-scroller" ref={this.autoScrollerRef}>
<ul>
{this.getWrappedData().map((content, index) => (
<Item
key={`auto-scroller-item-${index}`}
content={content}
itemGap={itemGap}
lineHeight={lineHeight}
/>
))}
</ul>
</div>
);
}
}
class Item extends Component {
static propTypes = {
content: PropTypes.object.isRequired,
itemGap: PropTypes.number,
lineHeight: PropTypes.number
};
render() {
const { content, itemGap = 10 } = this.props;
return (
<li
className="auto-scroller__item"
style={{ paddingRight: `${itemGap}px` }}
>
<div className="auto-scroller__item__content">
<img draggable={false} src={content.imgUrl} />
<div className="auto-scroller__item__content__title">
{content.title}
</div>
</div>
</li>
);
}
}
You can test with the demo from PlayCode (source code).
Just open the link with Safari on the iPhone.
What I observed was every time when it was on the boundary cases, the image started to flicker.
Further, if you swipe it with your finger forth and back on that point, the whole UI started to flicker. (see this screen recording) However, we didn't spot this glitch on Android devices.
Any possible solutions are welcome. Does anyone encounter something like this before?
removing overflow-y: hidden; and overflow-x: auto; from autoscroller.css
solved it on my end.
another solution would be to add z-index: 1; and scroll-behavior: smooth; to .auto-scroller
let me know if it worked!

React component does not update on set state

I am trying to conditionally render a component based on toggling of flag inside state. It looks like the state is getting updated but the component is not getting rendered. Can some one tell what is wring here. renderTree function updates the state, but render is not called then.
import React from "react";
import CheckboxTree from "react-checkbox-tree";
import "react-checkbox-tree/lib/react-checkbox-tree.css";
import { build } from "../data";
import { Input, Dropdown } from "semantic-ui-react";
import _ from "lodash";
class Widget extends React.Component {
constructor(props) {
super(props);
this.state = {
nodes: build(),
checked: [],
expanded: [],
isDropdownExpanded: false,
keyword: ""
};
}
onCheck = checked => {
this.setState({ checked }, () => {
console.log(this.state.checked);
});
};
onExpand = expanded => {
this.setState({ expanded }, () => {
console.log(this.state.expanded);
});
};
renderTree = () => {
this.setState(
prevState => {
return {
...prevState,
isDropdownExpanded: !prevState.isDropdownExpanded
};
},
() => {
console.log(this.state);
}
);
};
onSearchInputChange = (event, data, searchedNodes) => {
this.setState(prevState => {
if (prevState.keyword.trim() && !data.value.trim()) {
return {
expanded: [],
keyword: data.value
};
}
return {
expanded: this.getAllValuesFromNodes(searchedNodes, true),
keyword: data.value
};
});
};
shouldComponentUpdate(nextProps, nextState) {
if (this.state.keyword !== nextState.keyword) {
return true;
}
if (!_.isEqual(this.state.checked, nextState.checked)) {
return true;
}
if (_.isEqual(this.state.expanded, nextState.expanded)) {
return false;
}
return true;
}
getAllValuesFromNodes = (nodes, firstLevel) => {
if (firstLevel) {
const values = [];
for (let n of nodes) {
values.push(n.value);
if (n.children) {
values.push(...this.getAllValuesFromNodes(n.children, false));
}
}
return values;
} else {
const values = [];
for (let n of nodes) {
values.push(n.value);
if (n.children) {
values.push(...this.getAllValuesFromNodes(n.children, false));
}
}
return values;
}
};
keywordFilter = (nodes, keyword) => {
let newNodes = [];
for (let n of nodes) {
if (n.children) {
const nextNodes = this.keywordFilter(n.children, keyword);
if (nextNodes.length > 0) {
n.children = nextNodes;
} else if (n.label.toLowerCase().includes(keyword.toLowerCase())) {
n.children = nextNodes.length > 0 ? nextNodes : [];
}
if (
nextNodes.length > 0 ||
n.label.toLowerCase().includes(keyword.toLowerCase())
) {
n.label = this.getHighlightText(n.label, keyword);
newNodes.push(n);
}
} else {
if (n.label.toLowerCase().includes(keyword.toLowerCase())) {
n.label = this.getHighlightText(n.label, keyword);
newNodes.push(n);
}
}
}
return newNodes;
};
getHighlightText = (text, keyword) => {
const startIndex = text.indexOf(keyword);
return startIndex !== -1 ? (
<span>
{text.substring(0, startIndex)}
<span style={{ color: "red" }}>
{text.substring(startIndex, startIndex + keyword.length)}
</span>
{text.substring(startIndex + keyword.length)}
</span>
) : (
<span>{text}</span>
);
};
render() {
const { checked, expanded, nodes, isDropdownExpanded } = this.state;
let searchedNodes = this.state.keyword.trim()
? this.keywordFilter(_.cloneDeep(nodes), this.state.keyword)
: nodes;
return (
<div>
<Dropdown fluid selection options={[]} onClick={this.renderTree} />
{isDropdownExpanded && (
<div>
<Input
style={{ marginBottom: "20px" }}
fluid
icon="search"
placeholder="Search"
iconPosition="left"
onChange={(event, data) => {
this.onSearchInputChange(event, data, searchedNodes);
}}
/>
<CheckboxTree
nodes={searchedNodes}
checked={checked}
expanded={expanded}
onCheck={this.onCheck}
onExpand={this.onExpand}
showNodeIcon={true}
/>
</div>
)}
</div>
);
}
}
export default Widget;
Problem is in your shouldComponentUpdate method:
shouldComponentUpdate(nextProps, nextState) {
if (this.state.keyword !== nextState.keyword) {
return true;
}
if (!_.isEqual(this.state.checked, nextState.checked)) {
return true;
}
if (_.isEqual(this.state.expanded, nextState.expanded)) {
return false;
}
return true;
}
Since renderTree only changes isDropdownExpanded value, shouldComponentUpdate always returns false
If shouldComponenetUpdate returns true then your component re-renders, otherwise it dosen't.
In your code sandbox, it can be seen that every time you click on the dropdown, the shouldComponenetUpdate returns false for this condition
if (_.isEqual(this.state.expanded, nextState.expanded)) {
return false;
}
Either you need to change the state of this variable in your renderTree function or you need to re-write this condition as
if (_.isEqual(this.state.isDropdownExpanded, nextState.isDropdownExpanded)) {
return false;
}
Ciao, to force a re-render in React you have to use shouldComponentUpdate(nextProps, nextState) function. Something like:
shouldComponentUpdate(nextProps, nextState) {
return this.state.isDropdownExpanded !== nextState.isDropdownExpanded;
}
When you change isDropdownExpanded value, shouldComponentUpdate will be triggered and in case return is equal to true, component will be re-rendered. Here working example (based on your codesandbox).

Cannot get checkboxes of component to render based off state of another component

In my component did update, I console.log this.state.checkedTeams and this.state.checked. These values are determined through the map in componentDidUpdate. They read as they should read.
However, when I console log the values in side the render, I get empty arrays.
When I try to set the state in componentDidUpdate, I exceed the maximum depth and throw an error.
The goal of this to be able to adjust this.state.checked based off of the state of a foreign component.
import React, { Component } from 'react'
import { Text, View } from 'react-native'
import { connect } from 'react-redux'
import { loadTeams, loadLeagues } from '../actions'
import Check from './CheckBox'
class TeamSelect extends Component {
constructor(props) {
super(props)
this.state = {
checked: [],
checkedTeams: [],
setOnce: 0
}
}
componentDidUpdate() {
this.state.checked.length = 0
this.props.team.map(
(v, i) => {
if(this.props.checkedLeagues.includes(v.league.acronym)){
this.state.checked.push(true)
this.state.checkedTeams.push(v.team_name)
} else{
this.state.checked.push(false)
}
}
)
console.log('checkedTeams', this.state.checkedTeams)
console.log('checked', this.state.checked)
}
// componentDidUpdate(){
// if(this.state.checked.length === 0) {
// this.props.team.map(
// (v, i) => {
// if(this.props.checkedLeagues.includes(v.league.acronym)){
// this.state.checked.push(true)
// this.state.checkedTeams.push(v.team_name)
// } else{
// this.state.checked.push(false)
// }
// }
// )
// }
// console.log('checked league', this.props.checkedLeagues)
// }
changeCheck = (index, name) => {
firstString = []
if(!this.state.checkedTeams.includes(name)) {
firstString.push(name)
} else {
firstString.filter(v => { return v !== name})
}
this.state.checkedTeams.map(
(v, i) => {
if(v !== name) {
firstString.push(v)
}
}
)
if(name === this.state.checkedTeams[0] && firstString.length === 0) {
this.setState({ checkMessage: `Don't you want something to look at?` })
} else {
if(!this.state.checkedTeams.includes(name)){
this.state.checkedTeams.push(name)
} else {
this.setState({checkedTeams: this.state.checkedTeams.filter(v => { return v !== name})})
}
this.state.checked[index] = !this.state.checked[index]
this.setState({ checked: this.state.checked })
// queryString = []
// firstString.map(
// (v, i) => {
// if (queryString.length < 1) {
// queryString.push(`?league=${v}`)
// } else if (queryString.length >= 1 ) {
// queryString.push(`&league=${v}`)
// }
// }
// )
// axios.get(`http://localhost:4000/reports${queryString.join('')}`)
// .then(response => {
// this.props.loadCards(response.data)
// })
}
// console.log('first string', firstString)
// console.log('in function', this.state.checkedTeams)
}
render() {console.log('in render - checkedTeams', this.state.checkedTeams)
console.log('in render - checked', this.state.checked)
return(
<View>
{
this.props.team === null ?'' : this.props.team.map(
(v, i) => {
return(
<View key={i}>
<Check
checked={this.state.checked[i]}
index={i}
value={v.team_name}
changeCheck={this.changeCheck}
/>
{ v.team_name === undefined ? null :
<Text>{v.team_name}</Text>}
</View>
)
}
)
}
</View>
)
}
}
function mapStateToProps(state) {
return {
team: state.team.team,
checkedLeagues: state.league.checkedLeagues
}
}
export default connect(mapStateToProps)(TeamSelect)
You are mutating state directly in several places (very bad). You have to use this.setState(new_state) or react looses track of what is changing. In order to change the state of one component based on the value of another, you need to have the dependent component as a child of the control component so it can receive the value as a prop or you need to pass a state changing function to the controlling element that is bound to the dependent element.
Read the official react docs on how state works and lifting state for more info.
https://reactjs.org/docs/faq-state.html
https://reactjs.org/docs/lifting-state-up.html

React DND - Cannot add new item to state after a drag and drop event

I am working on a simple version of ReactDND before I implement this code into my image uploader.
Each time an image is added, it is added to state and passed through to ReactDND so that it is draggable and also droppable (so users can rearrange their images).
Everything works great, except for one thing. The problem I am having is after adding multiple images, is that once I drag and drop and image (works), the State no longer updates for ReactDND and I cannot add new images.
Here is my code below (note I am just using a button to add extra items to state):
Main Component:
import React from 'react';
// Drag and drop stuff
import { DragDropContext } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import Container from './Container';
class ImageUploader extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [],
listCount: 1
};
this.onAddItem = this.onAddItem.bind(this);
}
onAddItem(e) {
e.preventDefault();
var listArray = this.state.list;
var buildObject = {
text: 'Jeremy' + this.state.listCount.toString(),
age: '25',
id: this.state.listCount
};
listArray.push(buildObject);
let newListCount = this.state.listCount + 1;
this.setState({
list: listArray,
listCount: newListCount
});
console.log(this.state.list);
}
render() {
return (
<div>
<h1>Add to List</h1>
<button onClick={this.onAddItem}>Add Item</button>
<h1>The List</h1>
<Container id={1} list={this.state.list} />
</div>
)
}
}
export default DragDropContext(HTML5Backend)(ImageUploader);
Container:
import React, { Component } from 'react';
import update from 'react/lib/update';
import Card from './Card';
import { DropTarget } from 'react-dnd';
class Container extends Component {
constructor(props) {
super(props);
this.state = { cards: props.list };
}
pushCard(card) {
this.setState(update(this.state, {
cards: {
$push: [ card ]
}
}));
}
removeCard(index) {
this.setState(update(this.state, {
cards: {
$splice: [
[index, 1]
]
}
}));
}
moveCard(dragIndex, hoverIndex) {
const { cards } = this.state;
const dragCard = cards[dragIndex];
this.setState(update(this.state, {
cards: {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragCard]
]
}
}));
}
render() {
const { cards } = this.state;
const { canDrop, isOver, connectDropTarget } = this.props;
const isActive = canDrop && isOver;
const style = {
width: "200px",
height: "404px",
border: '1px dashed gray'
};
const backgroundColor = isActive ? 'lightgreen' : '#FFF';
return connectDropTarget(
<div className="houzes-dropbox">
{cards.map((card, i) => {
return (
<Card
key={card.id}
index={i}
listId={this.props.id}
card={card}
removeCard={this.removeCard.bind(this)}
moveCard={this.moveCard.bind(this)} />
);
})}
</div>
);
}
}
const cardTarget = {
drop(props, monitor, component ) {
const { id } = props;
const sourceObj = monitor.getItem();
if ( id !== sourceObj.listId ) component.pushCard(sourceObj.card);
return {
listId: id
};
}
}
export default DropTarget("CARD", cardTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
}))(Container);
Card:
import React, { Component } from 'react';
import { findDOMNode } from 'react-dom';
import { DragSource, DropTarget } from 'react-dnd';
import flow from 'lodash/flow';
const style = {
border: '1px dashed grey',
padding: '0.5rem 1rem',
margin: '.5rem',
backgroundColor: 'white',
cursor: 'move'
};
class Card extends Component {
render() {
const { card, isDragging, connectDragSource, connectDropTarget } = this.props;
const opacity = isDragging ? 0 : 1;
// Background URL
let backgroundUrl = {
backgroundImage: "url(" + "http://localhost:4000/uploads/2017/8/a3ff91dc-2f80-42f7-951a-e9a74bf954d7-1200x800.jpeg" + ")"
};
console.log(card);
return connectDragSource(connectDropTarget(
<div className={`uploadedImageWrapper col-md-6 col-sm-12`}>
<div className="uploadedImage">
<span style={backgroundUrl} />
{card.text}
{card.age}
</div>
</div>
));
}
}
const cardSource = {
beginDrag(props) {
return {
index: props.index,
listId: props.listId,
card: props.card
};
},
endDrag(props, monitor) {
const item = monitor.getItem();
const dropResult = monitor.getDropResult();
if ( dropResult && dropResult.listId !== item.listId ) {
props.removeCard(item.index);
}
}
};
const cardTarget = {
hover(props, monitor, component) {
const dragIndex = monitor.getItem().index;
const hoverIndex = props.index;
const sourceListId = monitor.getItem().listId;
// Don't replace items with themselves
if (dragIndex === hoverIndex) {
return;
}
// Determine rectangle on screen
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
// Get vertical middle
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
// Determine mouse position
const clientOffset = monitor.getClientOffset();
// Get pixels to the top
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
return;
}
// Dragging upwards
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
return;
}
// Time to actually perform the action
if ( props.listId === sourceListId ) {
props.moveCard(dragIndex, hoverIndex);
// Note: we're mutating the monitor item here!
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
monitor.getItem().index = hoverIndex;
}
}
};
export default flow(
DropTarget("CARD", cardTarget, connect => ({
connectDropTarget: connect.dropTarget()
})),
DragSource("CARD", cardSource, (connect, monitor) => ({
connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging()
}))
)(Card);
So just to recap, I can add items to state, and they become draggable and droppable. But after having dragged and dropped an element, I can no longer add anymore items to state.
Any ideas as to what the solution would be? What am I doing wrong?
Thank-you for looking through this, and any answers. Cheers.
#Notorious.
I have checked your code in my side and solved the issue.
When you drag and drop an element that changes the state of Container but not the state of ImageUploader.
So I made a function to inform the state of Container has changed.
Also I inserted componentWillReceiveProps() function to Container and updated the state of Container in that function.
Finally the problem solved.
Here's the changed code.
Main Component:
import React from 'react';
// Drag and drop stuff
import {DragDropContext} from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';
import Container from './Container';
class ImageUploader extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [],
listCount: 1
};
this.onAddItem = this
.onAddItem
.bind(this);
this.listChanged = this.listChanged.bind(this);
}
onAddItem(e) {
e.preventDefault();
var listArray = this.state.list;
var buildObject = {
text: 'Jeremy' + this
.state
.listCount
.toString(),
age: '25',
id: this.state.listCount
};
listArray.push(buildObject);
let newListCount = this.state.listCount + 1;
this.setState({list: listArray, listCount: newListCount});
}
listChanged(newList) {
this.setState({
list: newList
})
}
render() {
return (
<div>
<h1>Add to List</h1>
<button onClick={this.onAddItem}>Add Item</button>
<h1>The List</h1>
<Container id={1} list={this.state.list} listChanged={this.listChanged}/>
</div>
)
}
}
export default DragDropContext(HTML5Backend)(ImageUploader);
Container:
import React, { Component } from 'react';
import update from 'react/lib/update';
import Card from './Card';
import { DropTarget } from 'react-dnd';
class Container extends Component {
constructor(props) {
super(props);
this.state = { cards: this.props.list };
}
pushCard(card) {
this.setState(update(this.state, {
cards: {
$push: [ card ]
}
}));
}
removeCard(index) {
this.setState(update(this.state, {
cards: {
$splice: [
[index, 1]
]
}
}));
}
moveCard(dragIndex, hoverIndex) {
const { cards } = this.state;
const dragCard = cards[dragIndex];
this.setState(update(this.state, {
cards: {
$splice: [
[dragIndex, 1],
[hoverIndex, 0, dragCard]
]
}
}));
}
componentWillReceiveProps(nextProps) {
// You don't have to do this check first, but it can help prevent an unneeded render
if (nextProps.list !== this.state.cards) {
this.props.listChanged(this.state.cards);
}
}
render() {
const { cards } = this.state;
const { canDrop, isOver, connectDropTarget } = this.props;
const isActive = canDrop && isOver;
const style = {
width: "200px",
height: "404px",
border: '1px dashed gray'
};
const backgroundColor = isActive ? 'lightgreen' : '#FFF';
return connectDropTarget(
<div className="houzes-dropbox">
{cards.map((card, i) => {
return (
<Card
key={card.id}
index={i}
listId={this.props.id}
card={card}
removeCard={this.removeCard.bind(this)}
moveCard={this.moveCard.bind(this)} />
);
})}
</div>
);
}
}
const cardTarget = {
drop(props, monitor, component ) {
const { id } = props;
const sourceObj = monitor.getItem();
if ( id !== sourceObj.listId ) component.pushCard(sourceObj.card);
return {
listId: id
};
}
}
export default DropTarget("CARD", cardTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
}))(Container);
I am really happy if this helped you.
Thanks for reading my post.
Vladimir

Categories