Rethinking In React DnD

Chathurangi Shyalika
10 min readJan 2, 2018

--

This blog post brings details on the drag and drop context, React DnD used in the developed XACML Development Tool in WSO2 Identity Server team.

React DnD is a set of React higher-order components that help to build complex drag and drop interfaces while keeping user components decoupled. React DnD is hard to learn if you have never used it before. Unlike other drag and drop libraries available React DnD is a concrete library and it has the data-driven approach.

Installing React Dnd

npm install react-dnd

There are some terminologies you have to understand when working with React DnD. Let’s go through them.

  1. HTML5 Backend

HTML5 is the only officially supported backend for React DnD. It uses HTML5 drag and drop API. HTML Drag and Drop interfaces enable applications to use drag and drop features in browsers. With these features, the user can select draggable elements with a mouse, drag the elements to a droppable element, and drop those elements by releasing the mouse button.

In order to specify the backend and setting up the shared DnD state behind the scenes, the root component of the application has to be wrapped with DragDropContext.

Installing HTML5 backend

npm install react-dnd-html5-backend

Following code snippet shows how this backend is used in the tool.

import DragDropContext from ‘react-dnd’;
import HTML5Backend from ‘react-dnd-html5-backend’;
@DragDropContext(HTML5Backend)
export default class SortableTree extends React.Component {
/* … */}

Where SortableTree is the root component of the context.

2. Drag Events

HTML5 offers several drag events. All of the drag event types have an associated global event handler. We can summarize these events as below.

╔═══════════╦═════════════════╦═══════════════════════════════════╗               
║ Event ║On Event Handler ║ Description ║
╠═══════════╬═════════════════╬═══════════════════════════════════╣
║ dragstart ║ ondragstart ║Fired when user starts dragging an ║
║ ║ ║ element or text selection. ║
║-----------║-----------------║-----------------------------------║
║ drag ║ ondrag ║Fired when an element or text ║
║ ║ ║ selection is being dragged. ║ ║-----------║-----------------║-----------------------------------║
║ dragenter ║ ondragenter ║Fired when dragged element or text ║
║ ║ ║ selection enters a valid drop ║ ║ ║ ║ target. ║ ║-----------║-----------------║-----------------------------------║
║ dragexit ║ ondragexit ║ Fired when an element is no longer║
║ ║ ║ the drag operation's immediate ║ ║ ║ ║ selection target. ║
║-----------║-----------------║-----------------------------------║
║ dragleave║ ondragleave ║Fired when dragged element or text ║
║ ║ ║ selection leaves a valid drop ║ ║ ║ ║ target. ║
║-----------║-----------------║-----------------------------------║
║ dragover ║ ondragover ║Fired when an element or text ║
║ ║ ║ selection is being dragged over ║ ║ ║ ║ a valid drop target. ║
║-----------║-----------------║-----------------------------------║
║ drop ║ ondrop ║Fired when an element or text ║
║ ║ ║ selection is dropped on a valid ║ ║ ║ ║ drop target. ║
║-----------║-----------------║-----------------------------------║
║ dragend ║ ondragend ║Fired when a drag operation is ║
║ ║ ║ being ended. ║
╚═══════════╩═════════════════╩═══════════════════════════════════╝

If you are interested with HTML5 drag and drop API just go through this site.

3. Items and Types

React DnD uses data as the source of truth. That means when we drag something across the screen, we don’t say that a component, or a DOM node is being dragged. Instead, we say that an item of a certain type is being dragged.

An item is a plain JavaScript object describing what’s being dragged. For example, in our use case, when dragging XACML policy elements, an item looked like {name: ‘Target’} or {name:’Rule’}. Describing the dragged data as a plain object helps you keep the components decoupled and unaware of each other.

A type is a string (or a symbol) that uniquely identifies a whole class of items in our application. In our use case we have a ‘Box’ type representing the draggable tools.

The special feature of these types is that they let you specify which drag sources and drop targets are compatible. This feature has been used in XACML Development Tool. At first the Container component accepts and reacts to only tools Target, Rules and Obligations. Rule component accepts and reacts to only Conditions and Obligations.

4. Monitors

Drag and drop events are stateful. That state has to live somewhere. So how does React DnD exposes this state to the components? It’s done via Monitors. Monitors are few tiny wrappers that lies over the internal state of the application. The monitors let you update the props of your components in response to the drag and drop state changes.

5. Collecting Function

For each component that needs to track the drag and drop state, you can define a collecting function that retrieves the relevant bits of it from the monitors. React DnD then takes care of timely calling your collecting function and merging its return value into your components’ props.

function collect(monitor) {
return {
highlighted: monitor.canDrop(),
hovered: monitor.isOver()
};
}

6. Connectors

The connectors let you assign one of the predefined roles (a drag source, a drag preview, or a drop target) to the DOM nodes in your render function.

A connector is passed as the first argument to the collecting function described above. Let’s see how we can use it to specify the drop target:

function collect(connect, monitor) {
return {
highlighted: monitor.canDrop(),
hovered: monitor.isOver(),
connectDropTarget: connect.dropTarget()
};
}

7. What are Higher-Order Components (HOC) ?

A higher-order component is just a function that takes an existing component and returns another component that wraps it.

In React DnD, DragSource, DropTarget, DragLayer are higher-order components. Then what are these DragSource and DropTarget? Simply Drag Source is the actual item that is being dragged and Drop Target is the place where you drop the drag source. I will be explaining them in more detail in later.

Here’s how to wrap a component in a DragSource.

import { DragSource } from ‘react-dnd’;@DragSource(/* … */)
export default class Box {
/* … */}

Note the @ Annotation here. It’s an ES7 decorator. Decorators make it possible to annotate and modify classes and properties at design time.

XACML policy can be seen as a tree structure. When developing the drag and drop module of the tool, this tree structure was employed. Following is a high level tree diagram of the structure used in this module.

High level tree diagram of a XACML policy

Here the Container component is the root element. In the container, PolicyComponent component is being rendered. In the PolicyComponent, Rule, Target, Condition and Obligation can be dropped. RuleComponent component is being rendered inside Rule component. In the RuleComponent, Target, Condition and Obligation can be dropped.

Hence, the components which can be dragged and dropped that means Rule, Target, Condition and Obligation, should appear as Drag Sources in this use case. Components where the items are dropped that means Container and Rule should be Drop Targets. Now let’s see how these two play their role in the developed tool.

DragSource

In order to make a component draggable you must have to wrap that component with DragSource. DragSource is a higher-order component that accepts three required parameters and one optional parameter. In XACML Development Tool Drag and Drop module, the component Box which represent Tooling items which are being dragged is the respective DragSource.

import { DragSource } from ‘react-dnd’;@DragSource(type, spec, collect)
export default class Box {
/* … */}

Now let’s see what are the parameters the drag source accepts.

  • type: Required. Either a string, an ES6 symbol, or a function that returns either given the component’s props.
  • spec: Required. This must be a plain JavaScript object with a some allowed methods on it. It describes how the drag source reacts to the drag and drop events. I will explain more on the drag source specification in the next section.
  • collect: Required. This is the collecting function. It should return a plain object of the props to inject into your component. It receives two parameters: connect and monitor. I will explain more on the collecting function in the next section.
  • options: Optional. This must be a plain object.

Drag Source Specification

The second spec parameter listed above must be a plain object that implements the drag source specification. Below is the list of all methods that it may have.

Specification Methods

  • beginDrag(props, monitor, component): Required. When the dragging starts, beginDrag is called. You must return a plain JavaScript object describing the data being dragged. In this use case {name:’Target’} or {name:Rule} was returned through this method.
  • endDrag(props, monitor, component): Optional. When the dragging stops, endDrag is called.
  • canDrag(props, monitor): Optional. Use it to specify whether the dragging is currently allowed.
  • isDragging(props, monitor): Optional. By default, only the drag source that initiated the drag operation is considered to be dragging.

The above specification methods accepts three parameters as props, monitor and component.

  • props: are your component’s current props.
  • monitor: An instance of DragSourceMonitor. It is used to query the information on the current drag state, such as the currently dragged item and its type, the current and initial coordinates and offsets and whether it was dropped etc. For more information on monitor, read the DragSourceMonitor documentation.
  • component:It is the instance of your component. This is used to access the underlying DOM node for position or size measurements, and to call setState, and other needed component methods. Note that we do not include this in isDragging and canDrag methods as the instance may not be available by the time they are called.

The following code snippet shows how the DragSource is implemented through the application.

import React from ‘react’;
import PropTypes from ‘prop-types’;
import {DragSource} from ‘react-dnd’;
const boxSource = {
beginDrag(props) {
return {
name: props.name,
}
},
}
@DragSource(props => props.type, boxSource, (connect, monitor) => ({connectDragSource: connect.dragSource(),
isDragging: monitor.isDragging(),
}))
export default class Box extends React.Component {constructor(props) {
super(props)
this.state = {
connectDragSource: PropTypes.func.isRequired,
isDragging: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
isDropped: PropTypes.bool.isRequired
}}
render() {
const {name, value, isDropped, isDragging, connectDragSource} = this.props
const
opacity = isDragging ? 0.4 : 1
let border = ‘1px dashed gray’
let
backgroundColor = ‘white’
let
padding = ‘0.5rem 1rem’
let
marginRight = ‘1.5rem’
let
marginBottom = ‘0.5rem’
let
cursor = ‘move’
let
float = ‘left’
let
width = ‘75px’
return connectDragSource(
<div style={{
border,backgroundColor,padding,marginRight,marginBottom,cursor,float,opacity,width,}}>
{isDropped ? <s>{name}</s> : name}
</div>,)}}

DropTarget

In order to make a component react to the compatible items being dragged, hovered, or dropped on it, you must have to wrap that component with DropTarget. DropTarget is a higher-order component that accepts three required parameters and one optional parameter same as DragSource. In XACML Development Tool Drag and Drop module, the components PolicyComponent and RuleComponent, where the dragged items are being dropped are the respective DropTargets.

import { DropTarget } from ‘react-dnd’;@DropTarget(types, spec, collect)
export default class PolicyComponent {
/* … */}

Now let’s see what are the parameters the drop target accepts.

  • type: Required. This should be a string, an ES6 symbol, an array of either, or a function that returns either of those, given component’s props.
  • spec: Required. This must be a plain JavaScript object with a some allowed methods on it. It describes how the drop target reacts to the drag and drop events. I will explain more on the drop target specification in the next section.
  • collect: Required. This is the collecting function. It should return a plain object of the props to inject into your component. It receives two parameters: connect and monitor. I will explain more on the collecting function in the next section.
  • options: Optional. This must be a plain object.

Drag Source Specification

The second spec parameter listed above must be a plain object that implements the drag source specification. Below is the list of all methods that it may have.

Specification Methods

  • drop(props, monitor, component): Optional. This is called when a compatible item is dropped on the target.
  • hover(props, monitor, component): Optional. Called when an item is hovered over the component.
  • canDrop(props, monitor): Optional. Use it to specify whether the drop target is able to accept the item.

The above specification methods accepts three parameters as props, monitor and component.

  • props: are your component’s current props.
  • monitor: An instance of DropTargetMonitor. It is used to query the information about the current drag state, such as the currently dragged item and its type, the current and initial coordinates and offsets, whether it is over the current target, and whether it can be dropped etc. For more information on monitor, read the DropTargetMonitor documentation.
  • component:It is the instance of your component. This is used to access the underlying DOM node for position or size measurements, and to call setState, and other needed component methods. Note that we do not include this in canDrop method as the instance may not be available by the time they are called.

The following code snippet shows how the DropTarget is implemented through the application.

import React from ‘react’;
import PropTypes from ‘prop-types’;
import {DropTarget} from ‘react-dnd’;
import Target from ‘./Target’;
import Rule from ‘./Rule’;
const policyComponentTarget = {
drop(props, monitor) {
props.onDrop(monitor.getItem())},
}
@DropTarget(props => props.accepts, policyComponentTarget, (connect, monitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),}))
export default class PolicyComponents extends React.Component {
constructor(props) {
super(props);
this.state = {
connectDropTarget: PropTypes.func.isRequired,
isOver: PropTypes.bool.isRequired,
canDrop: PropTypes.bool.isRequired,
accepts: PropTypes.arrayOf(PropTypes.string).isRequired,
lastDroppedItem: PropTypes.object,
onDrop: PropTypes.func.isRequired,
name:’’,}}
renderDroppedItem(lastDroppedItem) {
let component;
console.log(lastDroppedItem);
switch (lastDroppedItem.name) {
case ‘Target’:
component=(<Target onDelete={this.props.onDelete} onSubmitTarget={this.props.onSubmitTarget} />);
break;
case ‘Rule’:
component = (<Rule onDelete={this.props.onDelete} onSubmitRule={this.props.onSubmitRule}
onSubmitCondition={this.props.onSubmitCondition} />);
break;
default: component = undefined;
}
console.log(component);
return component
}
render() {
const {
accepts,isOver,canDrop,connectDropTarget,lastDroppedItem,} = this.props
const isActive = isOver && canDrop
let backgroundColor = ‘rgb(95, 89, 89)’
if
(isActive) {
backgroundColor = ‘darkgreen’
} else if (canDrop) {
backgroundColor = ‘darkkhaki’
}
let height = ‘500px’
let
width = ‘1500px’
let
marginRight = ‘1.5rem’
let
marginBottom = ‘1.5rem’
let
color = ‘black’
let
padding = ‘1rem’
let
textAlign = ‘center’
let
fontSize = ‘2rem’
let
lineHeight = ‘normal’
let
float = ‘left’
return connectDropTarget(
<div style={{height,width,marginRight,marginBottom,color,padding,textAlign,fontSize,lineHeight,float,backgroundColor}}>
Policy component{isActive
? ‘Release to drop’
: `This component accepts: ${accepts.join(‘, ‘)}`}
{lastDroppedItem && (
<p>Last dropped: {JSON.stringify(lastDroppedItem)}</p>)}
{lastDroppedItem && this.renderDroppedItem(lastDroppedItem)}
</div>,
)}}

The class PolicyComponent is rendered inside Container Component. Item Types have been listed in a separate class as follows.

ItemTypes.js

export default {
TARGET: ‘target’,
CONDITION: ‘condition’,
RULE: ‘rule’,
OBLIGATION: ‘obligation’
}

Container.js

import ItemTypes from ‘./ItemTypes’;
import Rule from ‘./Rule’;
import Condition from ‘./Condition’;
import Target from ‘./Target’;
import Xml_view from ‘../containers/xml_view’;
import PropTypes from ‘prop-types’;
export default class Container extends React.Component {constructor(props) {
super(props)
this.state = {
policycomponents: [
{accepts: [ItemTypes.RULE, ItemTypes.TARGET], lastDroppedItem: null}
],
boxes: [
{name: ‘Target’, type: ItemTypes.TARGET},
{name: ‘Rule’, type: ItemTypes.RULE},
{name: ‘Condition’, type: ItemTypes.CONDITION},
{name: ‘Obligation’, type: ItemTypes.OBLIGATION}
],
droppedBoxNames: [],}}isDropped(boxName) {
return this.state.droppedBoxNames.indexOf(boxName) > -0
}}
render() {
const {
policycomponents
} = this.state
return
(
<div className=”container-fluid”>
<div style={{overflow: ‘hidden’, clear: ‘both’, background: this.context.color}}>
{policycomponents.map(({accepts, lastDroppedItem}, index) => (
<PolicyComponents
accepts=
{accepts}
lastDroppedItem={lastDroppedItem}
onDrop={item => this.handleDrop(index, item)}
key={index}
onDelete={this.onDelete}
/>: ‘’))}
</div></div>)}
handleDrop(index, item) {
const {name} = item
const droppedBoxNames = name ? {$push: [name]} : {}
console.log(‘drop’, index, item);
this.setState(
update(this.state, {
policycomponents: {
[index]: {
lastDroppedItem: {
$set: item,
},},},
droppedBoxNames},),)}onDelete() {
this.setState({showComponent: false});
}}

This post explained descriptively on how the drag and drop module was implemented in the XACML Development Tool. Hope you got a clear understanding about it through this blog post. If you have any issues or comments on this blog post please leave a comment below.

--

--