Sortable DnD
What this feature adds
Section titled “What this feature adds”@omnicajs/vue-remote/remote now exports three building blocks for sortable drag-and-drop:
RemoteSortableContainerRemoteSortableItemRemoteDragHandle
They let remote code describe sortable lists and boards, while the Vue host adapter performs hit-testing, pointer tracking, keyboard cancel, and DOM state updates on the host side.
You do not need to register extra host components for this feature. The host Vue runtime already understands the internal DnD bindings emitted by these remote wrappers.
Remote API surface
Section titled “Remote API surface”import { RemoteDragHandle, RemoteSortableContainer, RemoteSortableItem, type RemoteSortableEvent,} from '@omnicajs/vue-remote/remote'Core props:
RemoteSortableContainercontainerId: logical container key.orientation:'vertical' | 'horizontal'.accepts?: allowed itemtypevalues.disabled?: disables drop handling for the container.onDragenter,onDragleave,onDragmove,onDrop.
RemoteSortableItemitemId: stable item key.containerId: current container key.index: current visual order index.type: item kind used byaccepts.payload?: optional serializable payload returned in events.disabled?: disables dragging for this item.onDragstart,onDragend,onDragcancel.
RemoteDragHandlefor?: explicititemIdto start dragging.disabled?: disables pointer start on the handle.
All three components also accept as, so the rendered root can be a native tag or a host component root.
Host and remote responsibilities
Section titled “Host and remote responsibilities”- Remote code declares containers, items, handles, and reorder behavior.
- Host runtime resolves the real DOM elements behind those nodes.
- The host tracks a pointer session and computes
before,after, orinside. - Acceptance is based on
item.typeand the target containeraccepts. Escapecancels an active drag and emitsonDragcancel.
This split keeps reorder logic in remote state, while low-level DOM work stays on the trusted host side.
Event lifecycle
Section titled “Event lifecycle”- Pointer down on a
RemoteDragHandlestarts a pending session. - After the drag threshold is crossed,
onDragstartfires on the source item. - While moving, the target container receives
onDragenter,onDragmove, andonDragleave. - On pointer up over an accepted target, the container receives
onDrop. - The source item always finishes with
onDragend, oronDragcancelafterEscape/pointercancel.
RemoteSortableEvent contains the normalized drag state:
accepteditemId,type,payloadsourceContainerId,targetContainerIdtargetItemId,targetIndexplacementpointersessionId
Placement model
Section titled “Placement model”beforeandafterare used when hovering an existing item edge.insideis used when dropping into a container itself, including empty containers.targetIndexis already normalized for the target container order.foronRemoteDragHandleis optional when the handle is rendered inside the dragged item subtree.
Example: sortable backlog in a remote SFC
Section titled “Example: sortable backlog in a remote SFC”<script setup lang="ts">import { ref } from 'vue'import { RemoteDragHandle, RemoteSortableContainer, RemoteSortableItem, type RemoteSortableEvent,} from '@omnicajs/vue-remote/remote'
type Task = { id: string; title: string;}
const tasks = ref<Task[]>([ { id: 'task-1', title: 'Design DnD contract' }, { id: 'task-2', title: 'Cover host runtime' }, { id: 'task-3', title: 'Ship docs' },])
const moveTask = (event: RemoteSortableEvent) => { if (!event.accepted || event.targetIndex == null) { return }
const fromIndex = tasks.value.findIndex(task => task.id === event.itemId)
if (fromIndex < 0) { return }
const [task] = tasks.value.splice(fromIndex, 1) const nextIndex = Math.min(event.targetIndex, tasks.value.length)
tasks.value.splice(nextIndex, 0, task)}</script>
<template> <RemoteSortableContainer as="ul" class="task-list" container-id="backlog" :accepts="['task']" orientation="vertical" :on-drop="moveTask" > <RemoteSortableItem v-for="(task, index) in tasks" :key="task.id" as="li" class="task-row" container-id="backlog" :index="index" :item-id="task.id" :payload="{ title: task.title }" type="task" > <RemoteDragHandle as="button" class="task-handle" :for="task.id" > Drag </RemoteDragHandle>
<span>{{ task.title }}</span> </RemoteSortableItem> </RemoteSortableContainer></template>For kanban-style boards, use multiple RemoteSortableContainer blocks and update the source and target arrays inside onDrop.
Host DOM state for styling
Section titled “Host DOM state for styling”During drag, the host runtime adds state attributes to the real DOM:
- Static markers:
data-dnd-sortable-container,data-dnd-sortable-item,data-dnd-handle - Active session markers:
data-dnd-source,data-dnd-dragging,data-dnd-target,data-dnd-drag-over - Placement and acceptance markers:
data-dnd-placement,data-dnd-drop-allowed,data-dnd-drop-forbidden
That means you can style drag state from host CSS without leaking the internal transport props into the DOM.