Rich Drag-and-drop in React.js

In this example, I will show how drag-and-drop can be implemented simply and flexibly in a React.js application. You can check out the finished example in this code pen or on GitHub.

Our system introduces React components called Draggable and DropTarget, and allows us to apply constraints on which Draggable/DropTarget combinations are allowed. This sort of rules-based feedback is common in many drag-and-drop applications, and absolutely essential to the system we’re building at m›PATH.

The example is based on normal mouse events and sidesteps the HTML5 drag-and-drop API. The standard API works pretty well in modern browsers these days, and can often be the simplest way to get something working. However, implementation can be somewhat confusing, and the API gives us limited control over the drag image (the transparent copy of what's being dragged.) If you're interested in combining React and HTML5 drag-and-drop, check out this article by Daniel Stocks.

In the following, I will go through some of the interesting details about the approach.

Problem overview

For this example, our rules will be simple: We have green and blue SourceObjects, and we have DropTargets that accept blues, greens, both, or none.

As a drag starts, the dragged item is removed from the list and the cursor changes. The valid DropTargets are given a highlight treatment and the invalid ones get toned down. This helps the user by visualizing the otherwise unseen rules about which drags are allowed.

(Well, in this case I guess the rules are visible already—in many real-world cases they will not be as clear-cut as this, and we might not be able to color code them all.)

Once the drag is over a valid DropTarget, we update the target again to show what a drop here would result in. This helps the user determine if the effect is as intended, saving her from having to undo something unintended.

High-level overview of solution

To allow these interactions, we will define the following responsibilities.

  • Our root component Example will hold the state of the currentDragItem.
  • Green and blue SourceObjects will render using a custom Draggable component that will handle the drag-and-drop logistics.
  • The DropTarget components will be given a reference to the currentDragItem, and use it to update their appearance according to the current drag state.
  • The Draggable will let the application know when a drag starts and stops, while the DropTarget will let the application know about a completed (valid) drop.

Main application component

For the implementation, we'll use CoffeeScript and Lodash to cut down on the boilerplate. The main component looks like this:

{div, p} = React.DOM

document.addEventListener 'DOMContentLoaded', ->
  React.renderComponent Example(), document.body

Example = React.createClass
  getInitialState: ->
    currentDragItem: null

  render: ->
    div
      className: "dnd-example #{'dragging' if @state.currentDragItem}"
      children: [
        SourceObjects
          onDragStart: @onDragStart
          onDragStop: @onDragStop
        DropTargets
          currentDragItem: @state.currentDragItem
          onDrop: @onDrop
        @dropDescription()
      ]

  onDragStart: (details) ->
    @setState currentDragItem: details

  onDragStop: ->
    @setState currentDragItem: null

  onDrop: (target) ->
    @setState lastDrop:
      source: @state.currentDragItem
      target: target

  dropDescription: ->
    if drop = @state.lastDrop
      p
        className: 'drop-description'
        children: "Dropped source #{drop.source.type}-#{drop.source.index} 
                          on target #{drop.target.index}"

The interesting part here is that our root component, Example, is keeping track of the currentDragItem. This allows the information about what is currently being dragged to get propagated to any component that is interested.

The benefit of spreading such information is that we can use it to provide feedback to the user. For example, we might want to show a tooltip mid-drag that describes the result of the current drag in words, such as “Connect source object #2 with target #5.” This could help users learn and understand our app’s conceptual model.

The other part worth noting is that callbacks are passed down into child components so that they can notify the Example about the drag events we're interested in keeping track of.

Draggable “source objects”

SourceObjects = React.createClass
  render: ->
    div
      className: 'dnd-source-objects'
      children: for object, i in @objects()
        SourceObject
          type: object.type
          index: i + 1
          children: i + 1
          onDragStart: @props.onDragStart
          onDragStop: @props.onDragStop

  objects: ->
    _.flatten [
      { type: 'green' } for i in [0..2]
      { type: 'blue' } for i in [0..2]
    ]

SourceObject = React.createClass
  render: ->
    Draggable
      className: "dnd-source-object #{@props.type}"
      children: @props.children
      onDragStart: @props.onDragStart
      onDragStop: @props.onDragStop
      dragData: @dragData

  dragData: ->
    type: @props.type
    index: @props.index

Next up is our source objects, which represent the draggable objects we see on the left in the mockups.

In the render method of SourceObject, we can see the interface of our Draggable. A component that wants to become draggable should provide a prop callback dragData so that on successful drops, the root component will have the data it needs to determine what will happen next.

The “Draggable” component

LEFT_BUTTON = 0
DRAG_THRESHOLD = 3 # pixels

Draggable = React.createClass
  getInitialState: ->
    mouseDown: false
    dragging: false

  render: ->
    @transferPropsTo div
      style: @style()
      className: "dnd-draggable #{'dragging' if @state.dragging}"
      children: @props.children
      onMouseDown: @onMouseDown

  style: ->
    if @state.dragging
      position: 'absolute'
      left: @state.left
      top: @state.top
    else
      {}

  onMouseDown: (event) ->
    if event.button == LEFT_BUTTON
      event.stopPropagation()
      @addEvents()
      pageOffset = @getDOMNode().getBoundingClientRect()
      @setState
        mouseDown: true
        originX: event.pageX
        originY: event.pageY
        elementX: pageOffset.left
        elementY: pageOffset.top

  onMouseMove: (event) ->
    deltaX = event.pageX - @state.originX
    deltaY = event.pageY - @state.originY
    distance = Math.abs(deltaX) + Math.abs(deltaY)

    if !@state.dragging and distance > DRAG_THRESHOLD
      @setState dragging: true
      @props.onDragStart? @props.dragData?()

    if @state.dragging
      @setState
        left: @state.elementX + deltaX + document.body.scrollLeft
        top: @state.elementY + deltaY + document.body.scrollTop

  onMouseUp: ->
    @removeEvents()
    if @state.dragging
      @props.onDragStop()
      @setState dragging: false

  addEvents: ->
    document.addEventListener 'mousemove', @onMouseMove
    document.addEventListener 'mouseup', @onMouseUp

  removeEvents: ->
    document.removeEventListener 'mousemove', @onMouseMove
    document.removeEventListener 'mouseup', @onMouseUp

Finally, we get to the core of our system, the Draggable itself. Most of this code is concerned with keeping track of mousedown state and deltas from where the drag began.

It is worth noting here that when a potential drag starts, we add listeners to the document. If the listeners were to reside on the DOM node of this component, we would risk dragging “too fast” and losing our drag item.

It is also worth pointing out that using raw event listeners like this goes somewhat against the grain of React. We lose the benefits of synthetic (browser-normalized) events and automated event delegation, and it also makes the app slightly harder to understand, since the developer now needs to know the difference. If you have a better way of achieving the same effect, please let me know!

The “drop targets”

DropTargets = React.createClass
  render: ->
    div
      className: 'dnd-drop-targets'
      children: for target, i in @targets()
        DropTarget
          target: target
          index: i
          currentDragItem: @props.currentDragItem
          onDrop: @props.onDrop

  targets: ->
    [
      { accepts: ['blue'] }
      { accepts: ['green'] }
      { accepts: ['blue', 'green'] }
      { accepts: [] }
    ]

DropTargets is a simple wrapper that sets up the targets with their constraints. In order for them to be able to make decisions about how to present themselves, they need to know about the currentDragItem.

DropTarget = React.createClass
  getInitialState: ->
    hover: false

  render: ->
    div
      className: @classes()
      children: 'accepts ' + @acceptsDescription()
      onMouseEnter: => @setState hover: true
      onMouseLeave: => @setState hover: false
      onMouseUp: @onDrop

  classes: ->
    [
      'dnd-drop-target'
      "#{@props.target.accepts.join ' '}"
      'active' if @active()
      'active-green' if @active() and @props.currentDragItem.type == 'green'
      'active-blue' if @active() and @props.currentDragItem.type == 'blue'
      'disabled' if @disabled()
      'hover' if @state.hover
    ].join ' '

  active: ->
    item = @props.currentDragItem 
    item and item.type in @props.target.accepts

  disabled: ->
    item = @props.currentDragItem 
    item and item.type not in @props.target.accepts

  acceptsDescription: ->
    if @props.target.accepts.length > 0
      @props.target.accepts.join ' & '
    else
      'nothing'

  onDrop: ->
    if @active()
      @props.onDrop? index: @props.index + 1

Finally, most of the code in DropTarget is concerned with providing the user with good feedback about whether this target is valid or invalid, and whether a drop is about to take place if she releases the mouse button now.

Here's what the final result looks like, when treated with a nice little sprinkle of SASS styles. The source is available on GitHub as well.

Next steps

Although the solution outlined above works well, it is quite noisy and verbose. In a perfect world, it would be nice if we could skip some of the implementation details and instead have drag-and-drop be made to feel more native and declarative. For instance, it might look something like:

SourceObject
  draggable: true
  dragData: type: 'green'

DropTarget
  droppable: true
  constrainDrops: (dragData) -> dragData.type in ['green', 'blue']

Moving forward, it would be great to have a powerful, community standard drag-and-drop solution in our React UI toolbox. Please let me know in the comments if you're interested in joining forces to put something like this together!

Take care,
/Kent William

If you want more updates from me, I have an RSS feed and a Twitter profile, and if you'd like to get in touch—m›PATH is hiring—you can reach me at kentwilliam [ at ] gmail [ dot ] com.

Comments

Saving Time & Staying Sane? Pros & Cons of React.js

When I began working for m›PATH earlier this year, one of the exciting early tasks was putting together our new web app tech stack, which currently consists of Ruby/Sinatra, Sass/Autoprefixer, CoffeeScript—and React.js.

We're building an ambitious new web app, where the UI complexity represents most of the app's complexity overall. It includes a tremendous amount of UI widgets as well as a lot rules on what-to-show-when. This is exactly the sort of situation React.js was built to simplify.

Overall, we've had a great experience using React.js. In my experience, the biggest benefit of the framework is how it effectively makes obsolete a number of front-end concerns and problem domains.

Big win: Tighter coupling of markup and behavior

In React.js, markup and JavaScript behavior are defined together in the same file, and event handling is defined directly on the relevant DOM nodes—similarly to for instance Angular (as well as DHTML for those who remember).

Here's a simple example:

ToggleButton = React.createClass
  getInitialState: -> 
    active: false

  toggle: ->
    @setState active: not @state.active

  render: ->
    React.DOM.button
      className: 'active' if @state.active 
      onClick: @toggle   

So, how does this make our life easier?

  • We no longer need to query the DOM. As a result, we also no longer have to spend time thinking up the right selectors to get the elements we want.
  • There's no longer any way to have markup/event-handler mismatch. This makes it easier to iterate on CSS class names as well as markup.
  • In React, all events get delegated for free. As a result, event delegation is no longer something your team needs to worry about.

Jury's still out: CSS lives outside React.js

Given the maintenance wins outlined above, the natural next step might be to look at whether a component's CSS could also live together with the component.

In my experience, the number of lines of CSS often outnumber the JavaScript/markup two or three to one, so I'm not sure if it would be a great idea to include the CSS in the component file itself.

Having said that, there are “architecture smells” to the current solution:

  • With a coupled tuple of my_component.coffee and my_component.css files for each component in a project, it is easier for things to accidentally get out of sync when renaming or removing components.
  • If we want to import a third-party component, we will typically have to integrate several files.
  • To understand a UI component, it is often necessary to see its CSS. For instance, CSS transitions and animations, pointer-events and display control functionality, yet “live” inside our CSS.

In other words, I wonder what gains could be had from making CSS a more integrated part of React.js.

Big win: Cascading updates and functional thinking

React.js invites us to think about our web app's UI as a tree where each level decides how to delegate responsibilities down to its branches.

The mental model is beautifully simple: A component is basically a function that receives a set of @props (properties) and returns a description of how to render itself given those properties and its internal @state. As long as we stay true to this intended cascading architecture, we are guaranteed that the application is always up-to-date. React.js relieves us of the need to worry about updating the DOM.

When reasoning about your app, you can imagine that the entire application re-renders every time there is a change in state—whenever the user clicks on something, or new data is retrieved from the back end.

(In practice, React is a lot more efficient than that, performing a diff operation behind the scenes to avoid redrawing more than necessary.)

Other JavaScript frameworks like AngularJS and Ember.js provide similar mechanisms, but React's virtual DOM approach appears to have performance benefits as UIs grow more complex.

Jury's still out: Verbose propagation

In React.js, by default the children of a component know nothing about their parent. The parent has to explicitly tell them anything they need to know.

For example, imagine that we have an application with a comment form, and this form needs to know about the current user's login state:

Application = React.createClass
  render: ->
    Article
      user: Users.getCurrentUser() 

Article = React.createClass
  ..
  render: ->
    ArticleContent()
    CommentForm user: @props.user

This way of propagating knowledge though your application seems simple enough early on, but it can become a bit verbose and error-prone as the application grows. In my experience, the leaf node (CommentForm in the example) might easily appear 5–10 levels deep in the UI hierarchy, which means that we might end up with 5–10 instances of user: @props.user strewn out over our code base.

Often, there is more than just one piece of knowledge that need to get propagated out in this manner, and we end up with the following concerns:

  • The intermediate components now contain repetitive implementation details, which makes them harder to maintain.
  • The additional noise can make the core logic of these intermediate components harder to understand.

To avoid this, we might reach for React's built-in @transferPropsTo method. It can simplify the surface propagation for us, making this:

render: ->
  CommentForm 
    user: @props.user
    applicationColor: @props.applicationColor
    articleId: @props.articleId

into:

render: ->
  @transferPropsTo CommentForm()

However, this approach comes with its own drawbacks:

  • It is no longer easy to see what inputs are used by the CommentForm
  • If we have a lot of “global” knowledge that needs to be available throughout the application, we end up prefacing a lot of component calls with @transferPropsTo, which again makes the core logic slightly harder to discern.

As of now, propagating props explicitly might be the lesser of evils. Luckily, it seems the React team is aware of the issue and looking at ways to resolve it.

Big win: Thinking in components

The push towards UIs that are reusable and composable has turned out to be a major productivity win for us.

Of course, this is not an idea unique to React—we see the same idea practically all over the place: web components, Ember, Angular, Backbone and other frameworks all provide similar abstractions.

Suffice it to say that React makes it easy to take advantage of composability. For example, in our current application, we have these components:

  • Form.Dropdown: a button, which when clicked will show/hide a dropdown, similar to HTML’s <select> element.
  • Form.RadioSet: a set of <input type="radio"> elements with labels.

When the need came up to implement a custom drop down similar to a <select>, all we had to do was something like this:

Form.SelectDropdown = React.createClass
  render: ->
    Form.Dropdown
      className: 'form-select-dropdown'
      children: @transferPropsTo Form.RadioSet()

The only thing that remained was to add special styles for the form-select-dropdown class—and we were done.

Wrapping up

That's it! What has been your experience with React? Let me know in the comments!

If you want more updates from me, I have an RSS feed and a Twitter profile, and if you'd like to get in touch—m›PATH is hiring—you can reach me at kentwilliam [ at ] gmail [ dot ] com.

Take care,
/Kent William

Comments

The road that led me here

In the near future, this will hopefully become a useful repository for thoughts on interaction design and web engineering.

However, before we can get started, here's a quick bit about who I am.

In brief, I'm into UX, web technologies and videogames—in no particular order.

Currently I'm enjoying my work at m›PATH exploring a React.js/CoffeeScript and Ruby/Sinatra tech stack as well as a host of complex UX problems.

Previously, I worked for Fan TV doing web development and UX design for their cutting-edge web app and backend tools.

I also worked as a UI designer at Disney Interactive on Facebook-based social games like Threads of Mystery and World Series of Poker.

I have a Master's degree in Computer Science with specialization in interaction design from the University of Oslo. For my thesis, I designed and implemented a prototype “videogame for women” and learnt a lot about the ways this kind of gender-driven design can be problematized.

Comments