5

I'm trying to integrate React DnD using the List and ListItem of Material UI and, while dragging, the entire list is shown as dragged element. I have tried to follow to the best of my understanding the examples, and here is what I have

import React, { Component, PropTypes } from 'react';
import { Random } from 'meteor/random';
import LocalizedComponent from '/client/components/LocalizedComponent';
// MUI
import { List, ListItem } from 'material-ui/List';
// ---
import { DragDropContext, DragSource, DropTarget } from 'react-dnd';
import { findDOMNode } from 'react-dom';

import HTML5Backend from 'react-dnd-html5-backend';


const itemSource = {
  beginDrag(props) {
    return {
      id: props.id,
      index: props.index
    };
  },
};

const itemTarget = {
  hover(props, monitor, component) {
    const dragIndex = monitor.getItem().index;
    const hoverIndex = props.index;

    // 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
    props.onMoveItem(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;
  },
};


class SortableListComponent extends Component {

  render() {
    const { children, onMoveItem } = this.props;
    let index = 0;

    return (
      <List>
        { React.Children.map(children, child => React.cloneElement(child, {
          id: Random.id(),
          index: index++,
          onMoveItem: onMoveItem
        })) }
      </List>
    );
  }
}

SortableListComponent.propTypes = {
  onMoveItem: PropTypes.func.isRequired
};


class SortableListItemComponent extends Component {

  render() {
    const {
      id,
      index,
      isDragging,
      connectDragSource,
      connectDropTarget,
      onMoveItem,
      ...other
    } = this.props;
    const opacity = 1; // isDragging ? 0 : 1;

    return connectDragSource(connectDropTarget(
      <div style={{ opacity }}>
        <ListItem { ...other } disabled={ isDragging } />
      </div>
    ));
  }
}
SortableListItemComponent.propTypes = {
  connectDragSource: PropTypes.func.isRequired,
  connectDropTarget: PropTypes.func.isRequired,
  id: PropTypes.any.isRequired,
  index: PropTypes.number.isRequired,
  isDragging: PropTypes.bool.isRequired,
  onMoveItem: PropTypes.func.isRequired,
};


export const SortableList = DragDropContext(HTML5Backend)(SortableListComponent);

export const SortableListItem = DropTarget('SortableListItem', itemTarget, connect => ({
  connectDropTarget: connect.dropTarget(),
}))(DragSource('SortableListItem', itemSource, (connect, monitor) => ({
  connectDragSource: connect.dragSource(),
  isDragging: monitor.isDragging(),
}))(SortableListItemComponent));

Basically, I substitute List for SortableList and ListItem for SortableListItem, and this is what I see while dragging

enter image description here

What am I doing wrong?

Edit

For example, here is an example usage

<SortableList>
  { actions.map((action, index) => (
    <SortableListItem id={ action.name } key={ index }
      primaryText={ (index + 1) + '. ' + action.name }
      onTouchTap={ this.handleActionEdit.bind(this, index) }
    />
  )) }
</SortableList>

or

<SortableList>
  { actions.map((action, index) => (
    <SortableListItem id={ action.name } key={ action.name }
      primaryText={ (index + 1) + '. ' + action.name }
      onTouchTap={ this.handleActionEdit.bind(this, index) }
    />
  )) }
</SortableList>

etc.

It does not change a thing.

kursat
  • 1,059
  • 5
  • 15
  • 32
Yanick Rochon
  • 51,409
  • 25
  • 133
  • 214

3 Answers3

3

I also faced the similar issue and found that this does not works properly on chrome. Please refer https://github.com/react-dnd/react-dnd/issues/832 for the similar issue. The following comment by a user helped me figure out the issue.

@kaiomagalhaes In my case I was getting this issue due one of child element of row (cell content) in fact had height that was greater than row height, but was hidden by visibility: hidden in css. So dragSource had width of rows and height of hidden control.

I hope you'll find this helpful.

Deepanshu Arora
  • 375
  • 1
  • 5
  • 21
2

I also tried to make a sortable Material-UI <List> with React-DND and had the same issue, but the snapshot was of my entire page, not just the list!

Still working on solving it, but I did notice that the issue goes away if you use a regular <div> instead of <ListItem />. So I'm afraid this might be a Material-UI quirk. The easiest solution is probably just to avoid <ListItem>.

I did come up with a workaround, and will edit my answer if I find a better one:

Usage

import SortableList from './SortableList';

<SortableList
    items={[
        {id: 1, text: 'one'},
        {id: 2, text: 'two'},
        {id: 3, text: 'three'},
        {id: 4, text: 'four'},
    ]}
    onChange={items => {
        console.log(JSON.stringify(items));
        // flux action, etc. goes here
    }}
    listItemProps={(item, index) => {
        return {
            primaryText: item.name,
            hoverColor: 'green',
            // pass desired props to <ListItem> here
        };
    }}
/>

SortableList.jsx

import React, { Component, PropTypes } from 'react';
import update from 'react-addons-update';

import List, { ListItem } from 'material-ui/List';

import { findDOMNode } from 'react-dom';
import { DragDropContext, DragSource, DropTarget } from 'react-dnd';
import HTML5Backend from 'react-dnd-html5-backend';

const SortableListTypes = {
    ITEM: 'ITEM',
};

class SortableListItem extends Component {
    render() {
        const { connectDragSource, connectDragPreview, connectDropTarget, ...rest } = this.props;
        return (
            <ListItem
                id={this.props.id + '/' + this.props.index}
                key={this.props.id + '/' + this.props.index}
                ref={instance => {
                    if (!instance) {
                        return;
                    }
                    const node = findDOMNode(instance);
                    connectDragSource(node);
                    connectDropTarget(node);

                    const greatGrandChild = node.childNodes[0].childNodes[0].childNodes[0];
                    connectDragPreview(greatGrandChild);
                }}
                {...rest}
            />
        );
    }
}

SortableListItem = DropTarget(
    SortableListTypes.ITEM,
    {
        hover: (props, monitor, component) => {
            const dragIndex = monitor.getItem().index;
            const hoverIndex = props.index;

            if (dragIndex === hoverIndex) {
                return;
            }

            const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
            const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
            const clientOffset = monitor.getClientOffset();
            const hoverClientY = clientOffset.y - hoverBoundingRect.top;
            if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
                return;
            }
            if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
                return;
            }

            props.moveItem(dragIndex, hoverIndex);
            monitor.getItem().index = hoverIndex;
        },
        drop: props => props.dropHandler()
    },
    (connect, monitor) => {
        return {
            connectDropTarget: connect.dropTarget(),
        }
    }
)(DragSource(
    SortableListTypes.ITEM,
    {
        beginDrag: (props, monitor, component) => {
            return {
                id: props.id,
                index: props.index,
            }
        }
    },
    (connect, monitor) => {
        return {
            connectDragSource: connect.dragSource(),
            connectDragPreview: connect.dragPreview(),
        }
    }
)(SortableListItem));

class SortableList extends Component {
    constructor(props) {
        super(props);

        this.state = {
            items: _.clone(props.items),
        };

        this.moveItem = this.moveItem.bind(this);
        this.dropHandler = this.dropHandler.bind(this);
    }

    componentWillReceiveProps(nextProps) {
        this.setState({
            items: _.clone(nextProps.items),
        });
    }

    moveItem(fromIndex, toIndex) {
        const draggedItem = this.state.items[fromIndex];

        this.setState(update(this.state, {
            items: {
                $splice: [
                    [fromIndex, 1],
                    [toIndex, 0, draggedItem],
                ],
            },
        }));
    }

    dropHandler() {
        this.props.onChange(_.clone(this.state.items));
    }

    render() {
        return (
            <List>
                {this.state.items.map((item, index) => (
                    <SortableListItem
                        key={item.id}
                        index={index}
                        moveItem={this.moveItem}
                        dropHandler={this.dropHandler}
                        {...this.props.listItemProps(item, index)}
                    />
                ))}
            </List>
        )
    }
}

export default DragDropContext(HTML5Backend)(SortableList);

SortableListItem.jsx

import React, { Component, PropTypes } from 'react';

import { ListItem } from 'material-ui/List';

import { findDOMNode } from 'react-dom';
import { DragSource, DropTarget } from 'react-dnd';

const SortableListTypes = {
    ITEM: 'ITEM',
};

class SortableListItem extends Component {
    render() {
        const { connectDragSource, connectDragPreview, connectDropTarget, ...rest } = this.props;
        return (
            <ListItem
                id={this.props.id + '/' + this.props.index}
                key={this.props.id + '/' + this.props.index}
                ref={instance => {
                    if (!instance) {
                        return;
                    }
                    const node = findDOMNode(instance);
                    connectDragSource(node);
                    connectDropTarget(node);

                    const greatGrandChild = node.childNodes[0].childNodes[0].childNodes[0];
                    connectDragPreview(greatGrandChild);
                }}
                {...rest}
            />
        );
    }
}

export default DropTarget(
    SortableListTypes.ITEM,
    {
        hover: (props, monitor, component) => {
            const dragIndex = monitor.getItem().index;
            const hoverIndex = props.index;

            if (dragIndex === hoverIndex) {
                return;
            }

            const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
            const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
            const clientOffset = monitor.getClientOffset();
            const hoverClientY = clientOffset.y - hoverBoundingRect.top;
            if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
                return;
            }
            if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
                return;
            }

            props.moveItem(dragIndex, hoverIndex);
            monitor.getItem().index = hoverIndex;
        },
        drop: props => props.dropHandler()
    },
    (connect, monitor) => {
        return {
            connectDropTarget: connect.dropTarget(),
        }
    }
)(DragSource(
    SortableListTypes.ITEM,
    {
        beginDrag: (props, monitor, component) => {
            return {
                id: props.id,
                index: props.index,
            }
        }
    },
    (connect, monitor) => {
        return {
            connectDragSource: connect.dragSource(),
            connectDragPreview: connect.dragPreview(),
        }
    }
)(SortableListItem));

By no means a perfect solution, but hopefully it helps. If your items don't have an id property, you'd need to edit the key prop of SortableListItem within SortableList.render.

chrmcg
  • 21
  • 3
1

The problem can be how you are passing your id and key into your child component. Generating random id with React dnd ends ups to be buggy. What you should do is that you should have unique ids of each single item into your data, in your case children. An example is here: https://github.com/react-dnd/react-dnd/blob/master/examples/04%20Sortable/Simple/Container.js#L17

{ React.Children.map(children, child => React.cloneElement(child, {
    id: child.id,
    key: child.id,
    index: index++,
    onMoveItem: onMoveItem
})) }
Shota
  • 6,910
  • 9
  • 37
  • 67
  • I do not have ids for that data, it is from an array of sub-documents. I will try to see if I can generate a hash from the data, though... but this will not guarantee uniqueness of each items. – Yanick Rochon Feb 14 '17 at 21:34
  • Also, add key there, it should be the same as id. I've updated the answer. – Shota Feb 14 '17 at 21:36
  • I have both `key` and `id`, and it does not change a thing. Updating question to reflect this. – Yanick Rochon Feb 14 '17 at 21:37
  • Try to make key and id to have exactly the same value. E.g: id={ action.name } key={ action.name } – Shota Feb 14 '17 at 21:43
  • So I understand how the ghosted list item problem can be solved, thank you. However the image dragging is stil he entire list! – Yanick Rochon Feb 14 '17 at 22:00