Skip to content

TheFreeform

The main container component that manages all drag & drop state.

Basic Usage

vue
<TheFreeform v-model="items" @drop-into="onDropInto">
  <FreeformItem v-for="item in items" :key="item.id" :item="item" />
  <FreeformPlaceholder />
</TheFreeform>

Props

PropTypeDefaultDescription
modelValueFreeformItemData[]requiredItems array (v-model)
disabledbooleanfalseDisable all interactions
manualReorderbooleanfalseDon't auto-reorder, handle @reorder manually
dropZoneIdstring-Id of the surrounding FreeformDropZone, enabling drops from other lists

Events

EventPayloadDescription
update:modelValueitems[]Items array changed (reorder)
selectitems[]Selection changed
drag-startitems[]Drag operation started
drag-moveitems[], positionDragging (with cursor position)
drag-enditems[]Drag operation ended
drop-intoitems[], container, acceptedItems dropped into a container
reorderfromIndex, toIndexItems reordered
dropDropEventPayloadAny completed drop - carries dropType, indices and target
drop-to-zoneitems[], zoneId, index, containerIdItems dropped into another zone

Slots

Default Slot

Receives state information:

vue
<TheFreeform v-model="items">
  <template #default="{ items, selected, isDragging, dragItems, dropIndex }">
    <!-- Your content -->
  </template>
</TheFreeform>
PropTypeDescription
itemsarrayCurrent items
selectedarrayCurrently selected items
isDraggingbooleanDrag in progress
dragItemsarrayItems being dragged
dropIndexnumberWhere items will drop
dragSourceIndexnumberIndex the drag started from (-1 for incoming external drops)
isLassoActivebooleanLasso selection in progress

drag-ghost

Custom ghost displayed while dragging:

vue
<template #drag-ghost="{ items, count, position }">
  <div class="my-ghost">
    {{ items[0]?.name }}
    <span v-if="count > 1">+{{ count - 1 }}</span>
  </div>
</template>

CSS Variables

Set these on the container to customize colors:

css
.my-freeform {
  --freeform-color-primary: #3b82f6;
  --freeform-color-primary-light: #dbeafe;
  --freeform-color-success: #22c55e;
  --freeform-color-success-light: #dcfce7;
  --freeform-color-danger: #ef4444;
  --freeform-color-danger-light: #fee2e2;
  --freeform-color-neutral: #f3f4f6;
  --freeform-color-text: #374151;
}

Manual Reorder

Set manualReorder to handle reordering yourself:

vue
<TheFreeform
  v-model="items"
  :manual-reorder="true"
  @reorder="handleReorder"
>
ts
function handleReorder(fromIndex: number, toIndex: number) {
  // Custom reorder logic
  const item = items.value.splice(fromIndex, 1)[0]
  items.value.splice(toIndex, 0, item)
}

The drop event

@drop fires for every completed drop and tells you which kind it was, so you can handle reordering, folders and cross-list drops in one place:

ts
function onDrop(payload: DropEventPayload) {
  switch (payload.dropType) {
    case 'reorder':
      // payload.fromIndex / payload.toIndex
      break
    case 'container':
      // payload.targetContainer
      break
    case 'zone':
      // payload.targetZoneId / payload.targetContainerId
      break
  }
}

Event order

@drag-end fires before @drop. If you need state that gets cleared when the drag ends - a hovered grid cell, for example - stash it in @drag-end and read it in @drop.

Note that @drop does not fire for every gesture: a drag that ends without a valid target only emits @drag-end. Reset drag-related UI state there, not in @drop.

MIT Licensed