Search code examples
javascriptreactjsdrag-and-dropreact-dnd

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.


Solution

  • @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