Skip to content

Drag and Drop

@geajs/ui provides a drag-and-drop system built on native pointer events. It moves real DOM elements rather than cloning them, works across multiple containers, and animates drops with placeholder transitions.

Quick Start

There are two ways to use drag and drop: data attributes (simpler) or wrapper components (more structured).

Approach 1: Data Attributes

Add data-draggable-id to draggable elements and data-droppable-id to containers. The dndManager singleton auto-discovers them.

tsx
import { Component } from '@geajs/core'
import { dndManager } from '@geajs/ui'

export default class Board extends Component {
  created() {
    dndManager.onDragEnd = (result) => {
      store.moveItem(
        result.draggableId,
        result.destination.droppableId,
        result.destination.index
      )
    }
  }

  dispose() {
    dndManager.onDragEnd = null
    super.dispose()
  }

  template() {
    return (
      <div class="board">
        <div class="column" data-droppable-id="todo">
          {todoItems.map(item => (
            <div key={item.id} class="card" data-draggable-id={item.id}>
              {item.title}
            </div>
          ))}
        </div>
        <div class="column" data-droppable-id="done">
          {doneItems.map(item => (
            <div key={item.id} class="card" data-draggable-id={item.id}>
              {item.title}
            </div>
          ))}
        </div>
      </div>
    )
  }
}

Approach 2: Wrapper Components

Use DragDropContext, Droppable, and Draggable for a more declarative API.

tsx
import { Component } from '@geajs/core'
import { DragDropContext, Droppable, Draggable } from '@geajs/ui'

export default class Board extends Component {
  handleDragEnd(result) {
    store.moveItem(
      result.draggableId,
      result.destination.droppableId,
      result.destination.index
    )
  }

  template() {
    return (
      <DragDropContext onDragEnd={(r) => this.handleDragEnd(r)}>
        <Droppable droppableId="todo">
          {items.map(item => (
            <Draggable key={item.id} draggableId={item.id}>
              <div class="card">{item.title}</div>
            </Draggable>
          ))}
        </Droppable>
      </DragDropContext>
    )
  }
}

Exports

ts
import { dndManager, DragDropContext, Droppable, Draggable } from '@geajs/ui'
import type { DragResult } from '@geajs/ui'

DragResult

The onDragEnd callback receives a DragResult object:

ts
interface DragResult {
  draggableId: string
  source: { droppableId: string; index: number }
  destination: { droppableId: string; index: number }
}
FieldDescription
draggableIdThe data-draggable-id of the element that was dragged
source.droppableIdThe container it came from
source.indexIts original index in that container
destination.droppableIdThe container it was dropped into
destination.indexThe index where it was inserted

dndManager API

The dndManager is a singleton that manages all drag-and-drop interactions.

Property / MethodTypeDescription
onDragEnd(result: DragResult) => voidCallback when a drag completes. Set to null to disable.
isDraggingbooleantrue while a drag is in progress
registerDroppable(id, el)voidManually register a droppable container
unregisterDroppable(id)voidUnregister a droppable container
destroy()voidRemove all listeners and clear state

In most cases you only need to set onDragEnd — droppable containers are auto-discovered from data-droppable-id attributes when a drag starts.

Wrapper Components

DragDropContext

Wraps the drag-and-drop area. Calls dndManager.destroy() on dispose.

PropTypeDescription
onDragEnd(result: DragResult) => voidCalled when a drag completes
classstringCSS class for the wrapper div

Droppable

Marks a container as a drop target. Registers with dndManager automatically.

PropTypeDescription
droppableIdstringUnique identifier for this drop zone
classstringCSS class for the wrapper div

Draggable

Wraps a draggable item. Handles pointer events and delegates to dndManager.

PropTypeDescription
draggableIdstringUnique identifier for this draggable
indexnumberPosition index within its container
classstringCSS class for the wrapper div

Behavior

  • Drag threshold: A 5px movement is required before a drag starts, so clicks still work normally.
  • Escape to cancel: Press Escape during a drag to return the element to its original position.
  • Animated placeholders: When an element is dragged between containers, an animated placeholder shows where it will land.
  • DOM transfer: On drop, the actual DOM element is moved to its new position. The dndManager also updates Gea's internal component tree to keep the framework in sync.
  • Store updates: Use Store.silent() when reordering items in response to onDragEnd if the DOM has already been updated by the manager and you don't want the framework to re-patch the list.

Styling the Placeholder

The placeholder element has the class gea-dnd-placeholder. Style it to match your design:

css
.gea-dnd-placeholder {
  background: #e2e8f0;
  border-radius: 4px;
  border: 2px dashed #94a3b8;
}

Full Example: Kanban Board

See the Jira clone example for a complete kanban board with drag-and-drop across columns. The key files are:

  • Board.tsx — sets up dndManager.onDragEnd and renders columns
  • BoardColumn.tsx — uses data-droppable-id on the issue list container
  • IssueCard.tsx — uses data-draggable-id on each card

Released under the MIT License.