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
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.
react-dnd or @dnd-kit add hundreds of kilobytes to handle this.
Yichus uses style bindings -- zero library overhead, O(1) DOM updates.
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")
...
)
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.
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).
When Item 1 is dropped in Zone 1, three state writes occur:
| State | Old Value | New Value | DOM 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.
| Feature | Yichus | React + react-dnd |
|---|---|---|
| Mobile support | Built-in polyfill | Requires react-dnd-touch-backend |
| Bundle size | 0 KB (runtime only) | ~50 KB (react-dnd + backends) |
| DOM updates per drop | 3 property writes | Full subtree re-render |
| Drag state | IO[State[String]] | useDrag/useDrop hooks + context |
| Code complexity | ~150 lines Bosatsu | ~300+ lines with hooks/providers |