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/multiple-lists.md.

Multiple lists

Lists that share a group exchange items. Drag a row from one column to the other:

Live exampleTwo lists sharing a groupOpen in new tab
src/app/examples/multiple-lists/multiple-lists.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
  DragPreviewComponent,
  DraggableDirective,
  DropEvent,
  DroppableGroupDirective,
  moveItem,
  VirtualSortableListComponent,
} from 'ngx-virtual-dnd';

interface Task {
  id: string;
  title: string;
}

const createTasks = (prefix: string, count: number): Task[] =>
  Array.from({ length: count }, (_, i) => ({
    id: `${prefix}-${i + 1}`,
    title: `${prefix === 'todo' ? 'Task' : 'Done'} ${i + 1}`,
  }));

@Component({
  selector: 'app-multiple-lists-example',
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [
    VirtualSortableListComponent,
    DroppableGroupDirective,
    DraggableDirective,
    DragPreviewComponent,
  ],
  template: `
    <!-- Both lists share the "board" group, so items can move between them -->
    <div class="board" vdndGroup="board">
      <ng-template #taskTpl let-task>
        <div class="row" [vdndDraggable]="task.id">{{ task.title }}</div>
      </ng-template>

      <section class="column">
        <h3>
          To do <span>{{ todo().length }}</span>
        </h3>
        <vdnd-sortable-list
          class="list"
          droppableId="todo"
          [items]="todo()"
          [itemHeight]="48"
          [containerHeight]="288"
          [itemIdFn]="taskId"
          [itemTemplate]="taskTpl"
          (drop)="onDrop($event)"
        />
      </section>

      <section class="column">
        <h3>
          Done <span>{{ done().length }}</span>
        </h3>
        <vdnd-sortable-list
          class="list"
          droppableId="done"
          [items]="done()"
          [itemHeight]="48"
          [containerHeight]="288"
          [itemIdFn]="taskId"
          [itemTemplate]="taskTpl"
          (drop)="onDrop($event)"
        />
      </section>
    </div>

    <vdnd-drag-preview />
  `,
})
export class MultipleListsExampleComponent {
  readonly todo = signal<Task[]>(createTasks('todo', 200));
  readonly done = signal<Task[]>(createTasks('done', 200));

  readonly taskId = (task: Task): string => task.id;

  // (drop) fires on the destination list only, so both lists bind it.
  onDrop(event: DropEvent): void {
    moveItem(event, { todo: this.todo, done: this.done });
  }
}

Three details make this work:

  • Both <vdnd-sortable-list> elements are inside the same vdndGroup, and so is the shared item template.
  • Both lists bind (drop). The event fires on the destination list only, so a list without a handler silently rejects items.
  • moveItem() receives every list, keyed by droppableId. It handles moves within a list as well as between lists. If a droppable ID is missing from the map, the drop is ignored and a warning is logged in dev mode.

A board with any number of columns

Keep one signal per column and build the map for moveItem() from your column list:

interface Column {
  id: string;
  title: string;
  tasks: WritableSignal<Task[]>;
}

readonly columns: Column[] = [
  { id: 'todo', title: 'To do', tasks: signal([]) },
  { id: 'doing', title: 'In progress', tasks: signal([]) },
  { id: 'done', title: 'Done', tasks: signal([]) },
];

readonly lists = Object.fromEntries(this.columns.map((c) => [c.id, c.tasks]));

onDrop(event: DropEvent): void {
  moveItem(event, this.lists);
}
<div class="board" vdndGroup="board">
  <ng-template #taskTpl let-task>
    <div class="row" [vdndDraggable]="task.id">{{ task.title }}</div>
  </ng-template>

  @for (column of columns; track column.id) {
    <section>
      <h3>{{ column.title }}</h3>
      <vdnd-sortable-list
        [droppableId]="column.id"
        [items]="column.tasks()"
        [itemHeight]="48"
        [containerHeight]="480"
        [itemIdFn]="taskId"
        [itemTemplate]="taskTpl"
        (drop)="onDrop($event)"
      />
    </section>
  }
</div>

Draggable IDs must be unique across all columns, because drag state is shared across the whole app.

Controlling where items can go

GoalHow
Lists that never exchange itemsPut them in different groups (separate vdndGroup elements)
A list that temporarily accepts nothing[disabled]="true" on the list or on vdndDroppable
Items that cannot be moved[disabled]="true" on the draggable
Accept only some itemsBind disabled from the drag state, see below

A disabled droppable is skipped completely. The pointer passes through it to whatever is underneath, and the keyboard's left and right arrows skip it. See Drag behavior.

Releasing over nothing, or over a disabled list, emits no (drop). The item returns to where it started.

Accepting only some items

There is no predicate input that rejects some items. Returning early from (drop) keeps your data unchanged, but during the drag the target list still opens a slot for the item, so the user sees a drop that then does not happen.

Instead, disable the list while an item it does not accept is being dragged. DragStateService tells you which item that is, and a droppable whose disabled changes mid-drag is skipped from the next frame:

readonly #dragState = inject(DragStateService);

// Only tasks with an assignee can move to "Done"
readonly doneRejectsDrag = computed(() => {
  const task = this.#dragState.draggedItem()?.data as Task | undefined;
  return task !== undefined && !task.assignee;
});
<ng-template #taskTpl let-task>
  <div class="row" [vdndDraggable]="task.id" [vdndDraggableData]="task">{{ task.title }}</div>
</ng-template>

<vdnd-sortable-list droppableId="done" [disabled]="doneRejectsDrag()" ... />

The pointer then passes over the list without a placeholder, and the keyboard's left and right arrows skip it. Style .vdnd-droppable-disabled to show that the list does not accept the item.