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.