React-dnd: "canDrop", how to know what element we are on top of?

Created on 14 Apr 2016  ·  4Comments  ·  Source: react-dnd/react-dnd

In the drop(props, monitor, component) method, we have three arguments. But in the canDrop(props, monitor) there isn't the component arg.

I want that when an item is dragged over another component I "split" that component in two parts of 50% (height). If the item is dropped in the higher part then he'll be placed above it, if it's dropped in the lower part then it'll be placed after.

I already have the code that detects whether the component is dropped in the top/bottom part. What I want now is to add a class based on whether the mouse is in the top/bottom part (but during the hover action, not the drop action).

I don't know if I can use the hover(props, monitor, component) method, because I don't see how I'm supposed to add a property in my component (which would add a CSS class in the DOM or alike) from it, since it doesn't belong to the component itself and doesn't have access to its context and so on.

I guess I'm missing something. :)

Most helpful comment

OK, so the idea is that you want to set state in your component, and then when the state is set, you add the class.

Let's say you're dragging a Foo into a Bar:

import React, { Component } from "react";
import classNames from "classnames";

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

    this.state = {
      // Where a foo is hovering over the component.  One of null, "above", or "below"
      fooHover: null,
    };
  }

  render() {
    const { fooHover } = this.state;

    const classes = classNames("bar", {
      "bar-above": fooHover === "above",
      "bar-below": fooHover === "below",
    });
  }
}

The only question is how we set that state!

import { findDOMNode } from "react-dom";

const barTarget = {
  ...
  hover(props, monitor, component) {
    if (!monitor.canDrop()) {
      return;
    }

    const rawComponent = undecorate(component); // undecorate described below

    const { y } = monitor.getClientOffset();
    const { top, height } = findDOMNode(component).getBoundingClientRect();

    if (y < top + height/2) {
      rawComponent.setFooHover("above"); // setFooHover described below
    } else {
      rawComponent.setFooHover("below");
    }
  },
  ...
}

So basically, you want to call an instance method of your component, setFooHover, which will call setState. That should look like this for performance reasons:

  // In Bar
  setFooHover(fooHover) {
    if (fooHover !== this.state.fooHover) {
      this.setState({ fooHover });
    }
  }

The reason why I am calling undecorate above is that react-dnd uses higher order components, and we want the unwrapped components. This is an ugly complication of higher order components in my opinion. Maybe you could implement this instead by dispatching an action to your flux store or something, but in case you decide to do it like me, here's my undecorate:

function undecorate(component) {
  let curr = component;
  while (typeof curr.getDecoratedComponentInstance === "function") {
    curr = curr.getDecoratedComponentInstance();
  }

  return curr;
}

Now if you implement this, everything will be looking good except for one small problem. The class never goes away! No one ever calls setHoverState(null) (there's no such thing as an endHover function in your barTarget). The proper way to handle this is to use componentWillReceiveProps like this:

  componentWillReceiveProps(nextProps) {
    if (this.props.isFooOver && !nextProps.isFooOver) {
      this.setState({ fooHover: null });
    }
  }

(Don't forget to add { isFooOver: monitor.isOver() } in your collecting function)

Let me know how it goes!

All 4 comments

Hey, you're in luck! I've implemented almost exactly this before, so let me throw together some code.

OK, so the idea is that you want to set state in your component, and then when the state is set, you add the class.

Let's say you're dragging a Foo into a Bar:

import React, { Component } from "react";
import classNames from "classnames";

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

    this.state = {
      // Where a foo is hovering over the component.  One of null, "above", or "below"
      fooHover: null,
    };
  }

  render() {
    const { fooHover } = this.state;

    const classes = classNames("bar", {
      "bar-above": fooHover === "above",
      "bar-below": fooHover === "below",
    });
  }
}

The only question is how we set that state!

import { findDOMNode } from "react-dom";

const barTarget = {
  ...
  hover(props, monitor, component) {
    if (!monitor.canDrop()) {
      return;
    }

    const rawComponent = undecorate(component); // undecorate described below

    const { y } = monitor.getClientOffset();
    const { top, height } = findDOMNode(component).getBoundingClientRect();

    if (y < top + height/2) {
      rawComponent.setFooHover("above"); // setFooHover described below
    } else {
      rawComponent.setFooHover("below");
    }
  },
  ...
}

So basically, you want to call an instance method of your component, setFooHover, which will call setState. That should look like this for performance reasons:

  // In Bar
  setFooHover(fooHover) {
    if (fooHover !== this.state.fooHover) {
      this.setState({ fooHover });
    }
  }

The reason why I am calling undecorate above is that react-dnd uses higher order components, and we want the unwrapped components. This is an ugly complication of higher order components in my opinion. Maybe you could implement this instead by dispatching an action to your flux store or something, but in case you decide to do it like me, here's my undecorate:

function undecorate(component) {
  let curr = component;
  while (typeof curr.getDecoratedComponentInstance === "function") {
    curr = curr.getDecoratedComponentInstance();
  }

  return curr;
}

Now if you implement this, everything will be looking good except for one small problem. The class never goes away! No one ever calls setHoverState(null) (there's no such thing as an endHover function in your barTarget). The proper way to handle this is to use componentWillReceiveProps like this:

  componentWillReceiveProps(nextProps) {
    if (this.props.isFooOver && !nextProps.isFooOver) {
      this.setState({ fooHover: null });
    }
  }

(Don't forget to add { isFooOver: monitor.isOver() } in your collecting function)

Let me know how it goes!

Thanks a lot for the detailled explanation! I'll let you know :)

Finally, I used the Redux approach, so I basically dispatched an event through a reducer when the drop event was fired.

    drop: (propsTargetItem, monitor, targetItem) ->
        draggedItem = monitor.getItem()# Item that has been dragged and eventually dropped.
        coordsDrop = monitor.getClientOffset()# Coords of the drop action.
        diff = monitor.getDifferenceFromInitialOffset()# Comparison of the initial click (dragging start) against the end of the click (dropping start).

        patternId = draggedItem.patternId
        patternPosition = draggedItem.position
        targetId = propsTargetItem.context.patternId
        targetPosition = propsTargetItem.context.position
        isCursorAboveHalf = GeoService.isAboveHalf(ReactDOM.findDOMNode(targetItem), coordsDrop.y)

        # Extract positions in the array of children.
        oldPosition = draggedItem.position# Current position of the item, which will be the old position as soon as the item is moved. (by the server through sockets)
        newPosition = calcDropPosition(targetId, patternId, patternPosition, targetPosition, isCursorAboveHalf)# Position where we will move the dragged item.

        # Dispatch only if the position has changed and is set.
        if oldPosition isnt newPosition and newPosition?
            draggedItem.moveDnDPattern(Object.assign({}, draggedItem, {newPosition: newPosition, oldPosition: oldPosition}))

Was this page helpful?
0 / 5 - 0 ratings