← Back to Yichus

Cross-Platform Drag and Drop

Items move between zones when dragged and dropped -- on desktop via HTML5 drag events, and on mobile via a touch-to-drag polyfill. The item positions are tracked as reactive state, and the binding map updates only the affected style.display properties.

Desktop + Mobile Direct DOM Bindings No Re-renders

Live Demo

1

The Visibility Model

Each item is rendered in all three locations (source, zone 1, zone 2), but only one copy is visible at a time. Visibility is controlled by style-display bindings: "flex" to show, "none" to hide.

# Item 1 has three visibility states
i1_src_vis: IO[State[String]] = state("flex")   # visible in source
i1_z1_vis: IO[State[String]] = state("none")   # hidden in zone 1
i1_z2_vis: IO[State[String]] = state("none")   # hidden in zone 2

# The chip reads its visibility from state
def item_chip(label, color, vis, handler) -> VNode:
  h("div", [
    ("style-display", vis),  # bound to state!
    on_dragstart(handler)
  ], [text(label)])

This avoids conditional rendering entirely. Each style.display update is a single property write through the binding map -- no re-render needed.

Compare to React
In React, moving items between containers typically means removing from one array and adding to another, triggering re-renders of both containers. Libraries like react-dnd or @dnd-kit add hundreds of kilobytes to handle this. Yichus uses style bindings -- zero library overhead, O(1) DOM updates.
2

Drag Events

Three event handlers power the interaction: on_dragstart (user picks up item), on_dragover (item hovers over a zone), on_drop (item released over a zone).

def start_drag_1(_: Unit) -> IO[Unit]: (
  d <- dragging.flatMap()
  d.write("item1")  # record which item is moving
)

def drop_zone1(_: Unit) -> IO[Unit]: (
  d <- dragging.flatMap()
  which <- d.read().flatMap()
  match which:
    case "item1":
      show_item1_at("zone1")  # show in zone1, hide elsewhere
    case "item2":
      show_item2_at("zone1")
    ...
)
dragstart dragging = "item1" drop (zone1) i1_z1_vis = "flex" style.display = "flex"
3

Mobile: Touch-to-Drag Polyfill

HTML5 drag events don't fire on touch devices. Yichus includes a runtime polyfill that intercepts touch events on elements with data-ondragstart attributes:

// Runtime polyfill (generated automatically)
document.addEventListener('touchstart', (e) => {
  var el = e.target.closest('[data-ondragstart]');
  if (el) {
    // Fire the dragstart handler
    // Create visual clone that follows the finger
  }
});

document.addEventListener('touchend', (e) => {
  // Find element under finger with elementFromPoint
  // If it has data-ondrop, fire its drop handler
});

The polyfill creates a semi-transparent clone that follows the finger, then uses document.elementFromPoint() on touchend to find the drop target. The same Bosatsu handlers fire for both mouse and touch -- no duplicate code needed.

New touch event externals
In addition to the polyfill, Yichus now provides on_touchstart, on_touchmove, and on_touchend as first-class event handlers. These pass "x,y" coordinates as strings, enabling custom touch interactions beyond drag-and-drop (e.g., drawing, gesture recognition).
4

The Binding Map in Action

When Item 1 is dropped in Zone 1, three state writes occur:

StateOld ValueNew ValueDOM Effect
i1_src_vis"flex""none"Source chip hidden
i1_z1_vis"none""flex"Zone 1 chip shown
status_text"Dragging...""Item 1 dropped in Zone 1"Status updated

Each write looks up the binding map, finds the target element and property, and writes directly. Three state changes, three DOM property mutations, zero tree diffing.

5

Summary

FeatureYichusReact + react-dnd
Mobile supportBuilt-in polyfillRequires react-dnd-touch-backend
Bundle size0 KB (runtime only)~50 KB (react-dnd + backends)
DOM updates per drop3 property writesFull subtree re-render
Drag stateIO[State[String]]useDrag/useDrop hooks + context
Code complexity~150 lines Bosatsu~300+ lines with hooks/providers