> 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.

# Multiple lists

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


Live exampleTwo lists sharing a group[Open in new tab](/ngx-virtual-dnd/demo/examples/multiple-lists/)

```ts file="<root>/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:

```ts
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);
}
```

```angular-html
<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

| Goal                                    | How                                                                          |
| --------------------------------------- | ---------------------------------------------------------------------------- |
| Lists that never exchange items         | Put 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 items                  | Bind `disabled` from the drag state, see [below](#accepting-only-some-items) |

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](https://gultyayev.github.io/ngx-virtual-dnd/guide/features/drag-behavior.md#disabling).

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:

```ts
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;
});
```

```angular-html
<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.
