For AI agents: the complete documentation index is available at https://gultyayev.github.io/ngx-virtual-dnd/llms.txt, the full documentation bundle is available at https://gultyayev.github.io/ngx-virtual-dnd/llms-full.txt, and this page is available as Markdown at https://gultyayev.github.io/ngx-virtual-dnd/guide/essentials/handling-drops.md.

Handling drops

The library never mutates your data. When an item is dropped, the destination droppable emits a DropEvent, and you apply the move.

The drop event

interface DropEvent {
  source: {
    draggableId: string;
    droppableId: string;
    index: number; // where the item was
    data?: unknown; // vdndDraggableData
  };
  destination: {
    droppableId: string;
    placeholderId: string; // deprecated: always END_OF_LIST; use index for the position
    index: number; // where the item goes, after it is removed from the source
    data?: unknown; // vdndDroppableData (droppableData on vdnd-sortable-list)
  };
}

destination.index is the insertion index after the item is removed from its source. "Remove at source.index, then insert at destination.index" is always correct, whether the move stays in one list or crosses lists.

With virtual scrolling most items are not in the DOM, so always work from the event's indexes, never from DOM order.

Helpers

FunctionUse when
moveItem(event, lists)Your lists are WritableSignal<T[]>. Works within one list and across lists.
reorderItems(event, list)A single signal-based list. Reorders only.
applyMove(event, lists)Immutable state (a store, a service). Takes plain arrays and returns new ones.
isNoOpDrop(event)true when the item was dropped back where it started.
insertAt(list, item, index)Returns a new array with the item inserted.
removeAt(list, index)Returns a new array without the item at index.

Signals

onDrop(event: DropEvent): void {
  moveItem(event, { todo: this.todo, done: this.done });
}

Stores and immutable state

onDrop(event: DropEvent): void {
  if (isNoOpDrop(event)) return;

  const lists = applyMove(event, {
    todo: this.store.todo(),
    done: this.store.done(),
  });
  this.store.setLists(lists);
}

applyMove() returns a shallow copy of the record, with new arrays for the lists that changed.

Saving to a server

Apply the move straight away and roll back if the request fails:

onDrop(event: DropEvent): void {
  if (isNoOpDrop(event)) return;

  const previous = { todo: this.todo(), done: this.done() };
  moveItem(event, { todo: this.todo, done: this.done });

  this.api
    .moveTask(event.source.draggableId, event.destination.droppableId, event.destination.index)
    .subscribe({
      error: () => {
        this.todo.set(previous.todo);
        this.done.set(previous.done);
      },
    });
}

Drag lifecycle events

Draggables also emit (dragStart) and (dragEnd):

<div [vdndDraggable]="task.id" (dragStart)="onDragStart($event)" (dragEnd)="onDragEnd($event)">
  {{ task.title }}
</div>

(dragEnd) fires after every drag, dropped or not. Check event.destinationIndex === null to detect "no drop": the drag was cancelled (for example with Escape), or released outside every droppable or over a disabled one. Don't rely on cancelled, which is true only in the first case. DragEndEvent lists every way a drag can end without a drop.

During a drag, droppables and <vdnd-sortable-list> emit (placeholderMove) each time the placeholder moves within them. Use it for per-step feedback such as haptics. See Animations and haptics.

To react to drags anywhere in the app, for example to show a hint, inject DragStateService and read its signals, such as isDragging() and activeDroppableId(). See the API reference.