Welcome to the web blog
Bit pusher at Spotify. Previously Interactive News at the New York Times, U.S. Digital Service, and Code for America.
Last time we talked about Biblio, we had wired up React Drag and Drop to work correctly a single access. In this post, we are going to talk about handling drop events to make sure that the dragged item sticks to the correct place.
To start, let’s think about the existing DropTarget
on our Work
object:
const workDropTarget = {
hover: function(props, monitor, component) {
const item = monitor.getItem();
const draggedPosition = item.position;
const hoverPosition = props.position;
// find the middle of things
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
const clientOffset = monitor.getClientOffset();
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
// don't move until we are halfway over the card
if (draggedPosition < hoverPosition && hoverClientY < hoverMiddleY) return;
if (draggedPosition > hoverPosition && hoverClientY > hoverMiddleY) return;
// insert a display placeholder at an appropriate position
const dragDir = draggedPosition > hoverPosition ? 'up' : 'down';
props.setPlaceholder(draggedPosition, hoverPosition, dragDir);
}
}
This correctly handles dragging and putting the placeholder in the appropriate
place. We need to augment that to add the appropriate handlers for dropping down
our Work
:
const workDropTarget = {
hover: function(props, monitor, component) { ... },
drop: function(props, monitor) {
props.setPlaceholder(-1, -1, '')
const item = monitor.getItem()
props.moveWork(item.position, props.position, props.shelfNumber)
}
}
We do three things in this drop function:
setPlaceholder
is a simple
setState
operation that injects a placeholder
div into the appropriate
place in the array of Works
in our Shelf
.moveWork
. moveWork
is a standard redux action
that calls a reducer to update our application state. We’ll talk more about
how this works belowWhat is going on when we call moveWork
? We are dispatching an event which
triggers a reducer to reorganize the ordering of works in our shelf. Here is the
relevant reducer:
const newState = [...state]
const { lastWorkPos, nextWorkPos, shelfNumber } = action.payload;
const shelf = newState.find(function(e) { return e.id === shelfNumber })
shelf.works.splice(nextWorkPos, 0, lastShelf.works.splice(lastWorkPos, 1)[0]);
return newState
This gets the shelf from our list of shelves and updates that shelf’s works to reorder them properly.
One problem that we run into here is that if we drop over the placeholder, then
the action doesn’t actually fire. This is because right now we have the
placeholder set up as a straightforward html div
tag without an attached
DropTarget
. The way to fix that is to turn the placeholder into a proper
component and wire it up with a DropTarget:
class WorkPlaceholder extends React.Component {
render() {
return this.props.connectDropTarget(
<div className="bb-work bb-work-placeholder" />
);
}
}
WorkPlaceholder.propTypes = {
connectDropTarget: React.PropTypes.func,
setPlaceholder: React.PropTypes.func,
moveWork: React.PropTypes.func,
shelfNumber: React.PropTypes.number,
position: React.PropTypes.number,
};
const workPlaceholderTarget = {
drop: function(props, monitor) {
props.setPlaceholder(-1, -1, '')
const item = monitor.getItem()
props.moveWork(item.position, props.position, props.shelfNumber)
}
};
export default DropTarget(DRAG_WORK, workPlaceholderTarget, function(connect) {
return {
connectDropTarget: connect.dropTarget(),
};
})(WorkPlaceholder);
Then, in our ShelfList
component, we replace the references to the plain
placeholder div
with references instead to our new WorkPlaceholder
. This
then allows us to have DropTarget
s over both the cards themselves and the
placeholders. At this point, we should have a complete working single-axis drag
and drop component completely wired.
At this point, we can now drag and drop cards up and down a single list.
However, we want to be able to drag and drop them across multiple lists.
It turns out that this is a pretty straightforward modification to make. We just
need to keep track of the source and destination shelfNumber
in addition to
the source and destination position (which is what we are currently doing).
We need to modify the following:
workDragSource
to include references to both the original shelfNumber
and the original positionsetPlaceholder
references to be able to contain information about the
full matrix of positions and shelfNumbers, as opposed to just information
about the position to drop the placeholder.DropTargets
const { lastShelfId, lastWorkPos, nextShelfId, nextWorkPos } = action.payload;
const lastShelf = newState.shelves.find(function(e) { return e.id === lastShelfId; });
const nextShelf = newState.shelves.find(function(e) { return e.id === nextShelfId; });
// no X move: moving a work up/down in an existing shelf
if (lastShelfId === nextShelfId) {
lastShelf.works.splice(nextWorkPos, 0, lastShelf.works.splice(lastWorkPos, 1)[0]);
} else {
nextShelf.works.splice(nextWorkPos, 0, lastShelf.works[lastWorkPos]);
lastShelf.works.splice(lastWorkPos, 1);
}
return newState;
With everything completed, we should now be able to move works between shelves:
To see the full code, check it out on Github.
There are still a few things that we need to do to finish out this functionality:
setState
is a limitation for this because it doesn’t
allow cross-shelf communication. We will have to move this into a reducer
method.