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. :)
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}))
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:
The only question is how we set that state!
So basically, you want to call an instance method of your component,
setFooHover
, which will callsetState
. That should look like this for performance reasons: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 myundecorate
: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 usecomponentWillReceiveProps
like this:(Don't forget to add
{ isFooOver: monitor.isOver() }
in your collecting function)Let me know how it goes!