I implemented react-dnd, a flexible HTML5 drag-and-drop mixin for React with full DOM control.
Existing drag-and-drop libraries didn't fit my use case so I wrote my own. It's similar to the code we've been running for about a year on Stampsy.com, but rewritten to take advantage of React and Flux.
Key requirements I had:
If these sound familiar to you, read on.
First, declare types of data that can be dragged.
These are used to check “compatibility” of drag sources and drop targets:
// ItemTypes.js
module.exports = {
BLOCK: 'block',
IMAGE: 'image'
};
(If you don't have multiple data types, this libary may not be for you.)
Then, let's make a very simple draggable component that, when dragged, represents IMAGE
:
var { DragDropMixin } = require('react-dnd'),
ItemTypes = require('./ItemTypes');
var Image = React.createClass({
mixins: [DragDropMixin],
configureDragDrop(registerType) {
// Specify all supported types by calling registerType(type, { dragSource?, dropTarget? })
registerType(ItemTypes.IMAGE, {
// dragSource, when specified, is { beginDrag(), canDrag()?, endDrag(didDrop)? }
dragSource: {
// beginDrag should return { item, dragOrigin?, dragPreview?, dragEffect? }
beginDrag() {
return {
item: this.props.image
};
}
}
});
},
render() {
// {...this.dragSourceFor(ItemTypes.IMAGE)} will expand into
// { draggable: true, onDragStart: (handled by mixin), onDragEnd: (handled by mixin) }.
return (
<img src={this.props.image.url}
{...this.dragSourceFor(ItemTypes.IMAGE)} />
);
}
);
By specifying configureDragDrop
, we tell DragDropMixin
the drag-drop behavior of this component. Both draggable and droppable components use the same mixin.
Inside configureDragDrop
, we need to call registerType
for each of our custom ItemTypes
that component supports. For example, there might be several representations of images in your app, and each would provide a dragSource
for ItemTypes.IMAGE
.
A dragSource
is just an object specifying how the drag source works. You must implement beginDrag
to return item that represents the data you're dragging and, optionally, a few options that adjust the dragging UI. You can optionally implement canDrag
to forbid dragging, or endDrag(didDrop)
to execute some logic when the drop has (or has not) occured. And you can share this logic between components by letting a shared mixin generate dragSource
for them.
Finally, you must use {...this.dragSourceFor(itemType)}
on some (one or more) elements in render
to attach drag handlers. This means you can have several “drag handles” in one element, and they may even correspond to different item types. (If you're not familiar with JSX Spread Attributes syntax, check it out).
Let's say we want ImageBlock
to be a drop target for IMAGE
s. It's pretty much the same, except that we need to give registerType
a dropTarget
implementation:
var { DragDropMixin } = require('react-dnd'),
ItemTypes = require('./ItemTypes');
var ImageBlock = React.createClass({
mixins: [DragDropMixin],
configureDragDrop(registerType) {
registerType(ItemTypes.IMAGE, {
// dropTarget, when specified, is { acceptDrop(item)?, enter(item)?, over(item)?, leave(item)? }
dropTarget: {
acceptDrop(image) {
// Do something with image! for example,
DocumentActionCreators.setImage(this.props.blockId, image);
}
}
});
},
render() {
// {...this.dropTargetFor(ItemTypes.IMAGE)} will expand into
// { onDragEnter: (handled by mixin), onDragOver: (handled by mixin), onDragLeave: (handled by mixin), onDrop: (handled by mixin) }.
return (
<div {...this.dropTargetFor(ItemTypes.IMAGE)}>
{this.props.image &&
<img src={this.props.image.url} />
}
</div>
);
}
);
Say we now want the user to be able to drag out an image out of ImageBlock
. We just need to add appropriate dragSource
to it and a few handlers:
var { DragDropMixin } = require('react-dnd'),
ItemTypes = require('./ItemTypes');
var ImageBlock = React.createClass({
mixins: [DragDropMixin],
configureDragDrop(registerType) {
registerType(ItemTypes.IMAGE, {
// Add a drag source that only works when ImageBlock has an image:
dragSource: {
canDrag() {
return !!this.props.image;
},
beginDrag() {
return {
item: this.props.image
};
}
}
dropTarget: {
acceptDrop(image) {
DocumentActionCreators.setImage(this.props.blockId, image);
}
}
});
},
render() {
return (
<div {...this.dropTargetFor(ItemTypes.IMAGE)}>
{/* Add {...this.dragSourceFor} handlers to a nested node */}
{this.props.image &&
<img src={this.props.image.url}
{...this.dragSourceFor(ItemTypes.IMAGE)} />
}
</div>
);
}
);
I have not covered everything but it's possible to use this API in a few more ways:
getDragState(type)
and getDropState(type)
to learn if dragging is active and use it to toggle CSS classes or attributes;dragPreview
to be Image
to use images as drag placeholders (use ImagePreloaderMixin
to load them);ImageBlocks
reorderable. We only need them to implement dropTarget
and dragSource
for ItemTypes.BLOCK
.dropTargetFor(...types)
allows to specify several types at once, so one drop zone can catch many different types.For up-to-date documentation and installation instructions, head to react-dnd repo on Github.