react-dnd
HTML5 drag-and-drop mixin for React with full DOM control.
Prior Work
Check these first and see if they fit your use case.
If they don't, read on.
Installation
npm install --save react-dnd
Dependencies: Flux and a couple of functions from lodash-node;
Peer Dependencies: React >= 0.11.0.
Note: I'm using ES6 features in this library, so you may want to enable Harmony transforms in JSX build step. This library has to be used with a bundler such as Webpack (or Browserify).
Rationale
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:
- Emit zero DOM or CSS of its own, leaving it to the consuming components;
- Impose as little structure as possible on consuming components;
- Use HTML5 drag and drop as primary backend but make it possible to add different backends in the future;
- Like original HTML5 API, emphasize dragging data and not just “draggable views”;
- Hide HTML5 API quirks from the consuming code;
- Different components may be “drag sources” or “drop targets” for different kinds of data;
- Allow one component to contain several drag sources and drop targets when needed;
- Make it easy for drop targets to change their appearance if compatible data is being dragged or hovered;
- Make it easy to use images for drag thumbnails instead of element screenshots, circumventing browser quirks.
Hopefully the resulting API reflects that.
Usage
Simple Drag Source
First, declare types of data that can be dragged.
These are used to check “compatibility” of drag sources and drop targets:
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) {
registerType(ItemTypes.IMAGE, {
dragSource: {
beginDrag() {
return {
item: this.props.image
};
}
}
});
},
render() {
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 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 mixins 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).
Simple Drop Target
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: {
acceptDrop(image) {
DocumentActionCreators.setImage(this.props.blockId, image);
}
}
});
},
render() {
return (
<div {...this.dropTargetFor(ItemTypes.IMAGE)}>
{this.props.image &&
<img src={this.props.image.url} />
}
</div>
);
}
);
Drag Source + Drop Target In One Component
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, {
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>
);
}
);
What Else Is Possible?
I have not covered everything but it's possible to use this API in a few more ways:
- Use
getDragState(type)
and getDropState(type)
to learn if dragging is active and use it to toggle CSS classes or attributes; - Specify
dragPreview
to be Image
to use images as drag placeholders (use ImagePreloaderMixin
to load them); - Say, we want to make
ImageBlock
s reorderable. We only need them to implement dropTarget
and dragSource
for ItemTypes.BLOCK
. - Suppose we add other kinds of blocks. We can reuse their reordering logic by placing it in a mixin.
dropTargetFor(...types)
allows to specify several types at once, so one drop zone can catch many different types.- When you need more fine-grained control, most methods are passed drag event that caused them as the last parameter.
API
require('react-dnd').DragDropMixin
configureDragDrop(registerType)
Gives you a chance to configure drag and drop on your component.
Components with DragDropMixin
will have this method.
registerType(type, { dragSource?, dropTarget? })
Call this method to specify component behavior as drag source or drop target for given type.
This method is passed as a parameter to configureDragDrop
.
getDragState(type)
Returns { isDragging: bool }
describing whether a particular type is being dragged from this component's drag source. You may want to call this method from render
, e.g. to hide an element that is being dragged.
getDropState(type)
Returns { isDragging: bool, isHovering: bool }
describing whether a particular type is being dragged or hovered, when it is compatible with this component's drop source. You may want to call this method from render
, e.g. to highlight drop targets when they are comparible and when they are hovered.
===================
Drag Source API
Specifies drag behavior of a component in this object.
-
beginDrag()
— return value must contain item
with an object representing your data and may also contain dragPreview: Image
, dragOrigin: DragOrigins
.
-
canDrag()
— optionally decide whether to allow dragging.
-
endDrag(didDrop)
— optionally handle end of dragging operation.
===================
Drop Target API
Specifies drag behavior of a component in this object.
enter(item)
, leave(item)
, over(item)
— optionally implement these to perform side effects (e.g. might use over
for reordering items when they overlap). If you need to render different states when drop target is active or hovered, it is easier to use this.getDropState(type)
in render
method.acceptDrop(item)
— optionally implement this method to perform some action when drop occurs.
===================
require('react-dnd').ImagePreloaderMixin
TODO: describe how to use it for preloading drag thumbnails
Examples
If you want to play with this library, consider helping me build a sample app for it.
Known Issues
Thanks
This library is a React port of an API, parts of which were originally written by Andrew Kuznetsov.