Skip to content

useSortable

A composable that leverages the HTML Drag and Drop API to provide a simple and flexible way to reorder items.

Import

ts
import useSortable from '@leaflink/stash/useSortable';

Usage

By just informing the parent reference and the list of items, the composable will automatically handle drag and drop events.

INFO

The consumer component must define which children are draggable by setting the draggable="true" attribute.

vue
<script setup lang="ts">
  import { ref } from 'vue';

  import useSortable from '../../../src/composables/useSortable/useSortable';

  const items = ref(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
  const containerRef = ref<HTMLElement | null>(null);
  useSortable({ ref: containerRef, list: items });
</script>

<template>
  <div class="tw-space-y-4">
    <div ref="containerRef" class="tw-space-y-2">
      <div
        v-for="item in items"
        :key="`dragging_classes_${item}`"
        class="tw-cursor-move tw-rounded tw-border tw-bg-white tw-p-4 hover:tw-border-blue-500"
        draggable="true"
      >
        {{ item }}
      </div>
    </div>

    <div>
      <p>Items list</p>
      <div>{{ JSON.stringify(items, null, 2) }}</div>
    </div>
  </div>
</template>
Item 1
Item 2
Item 3
Item 4

Items list

[ "Item 1", "Item 2", "Item 3", "Item 4" ]

Dragging classes

The ghostClass and chosenClass options allow you to customize the appearance of the ghost and chosen elements.

Item 1
Item 2
Item 3
Item 4

Items list

[ "Item 1", "Item 2", "Item 3", "Item 4" ]
vue
<script setup lang="ts">
  import { ref } from 'vue';

  import useSortable from '../../../src/composables/useSortable/useSortable';

  const items = ref(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
  const containerRef = ref<HTMLElement | null>(null);
  useSortable({
    ref: containerRef,
    list: items,
    ghostClass: 'tw-border-blue tw-border-4',
    chosenClass: 'tw-opacity-0',
  });
</script>

<template>
  <div class="tw-space-y-4">
    <div ref="containerRef" class="tw-space-y-2">
      <div
        v-for="item in items"
        :key="`dragging_classes_${item}`"
        class="tw-cursor-move tw-rounded tw-border tw-bg-white tw-p-4 hover:tw-border-blue-500"
        draggable="true"
      >
        {{ item }}
      </div>
    </div>

    <div>
      <p>Items list</p>
      {{ JSON.stringify(items, null, 2) }}
    </div>
  </div>
</template>

Disabling dragging

The composable requires the parent element to define which children are draggable by setting the draggable="true" attribute. If you want to disable dragging for a specific element, just add draggable="false" or remove the attribute from the element.

Item 1
Item 2
Not Draggable 1
Not Draggable 2

Items list

[ "Item 1", "Item 2" ]
vue
<script setup lang="ts">
  import { ref } from 'vue';

  import useSortable from '../../../src/composables/useSortable/useSortable';

  const items = ref(['Item 1', 'Item 2']);
  const containerRef = ref<HTMLElement | null>(null);
  useSortable({ ref: containerRef, list: items });
</script>

<template>
  <div class="tw-space-y-4">
    <div ref="containerRef" class="tw-space-y-2">
      <div
        v-for="drag in items"
        :key="drag"
        class="tw-cursor-move tw-rounded tw-border tw-bg-white tw-p-4 hover:tw-border-blue-500"
        draggable="true"
      >
        {{ drag }}
      </div>
      <div
        v-for="disabledDrag in 2"
        :key="`disabled-drag_${disabledDrag}`"
        class="tw-rounded tw-border tw-bg-white tw-p-4"
      >
        Not Draggable {{ disabledDrag }}
      </div>
    </div>

    <div>
      <p>Items list</p>
      <div>{{ JSON.stringify(items, null, 2) }}</div>
    </div>
  </div>
</template>

Sort in place

By default sortInPlace is set to true, which means the list will be sorted in place. If you want to disable this behavior, set it to false.

Item 1
Item 2
Item 3
Item 4

Items list

[ "Item 1", "Item 2", "Item 3", "Item 4" ]
vue
<script setup lang="ts">
  import { ref } from 'vue';

  import useSortable from '../../../src/composables/useSortable/useSortable';

  const items = ref(['Item 1', 'Item 2', 'Item 3', 'Item 4']);
  const containerRef = ref<HTMLElement | null>(null);
  useSortable({
    ref: containerRef,
    list: items,
    sortInPlace: false,
    ghostClass: 'tw-border-blue tw-border-4',
    chosenClass: 'tw-opacity-0',
  });
</script>

<template>
  <div class="tw-space-y-4">
    <div ref="containerRef" class="tw-space-y-2">
      <div
        v-for="item in items"
        :key="`dragging_classes_${item}`"
        class="tw-cursor-move tw-rounded tw-border tw-bg-white tw-p-4 hover:tw-border-blue-500"
        draggable="true"
      >
        {{ item }}
      </div>
    </div>

    <div>
      <p>Items list</p>
      <div>{{ JSON.stringify(items, null, 2) }}</div>
    </div>
  </div>
</template>

Config

ts

export interface UseSortableOptions<SortableItem> {
  /**
   * The parent element of the sortable elements
   */
  ref: Ref<HTMLElement | null>;

  /**
   * The list of sortable elements to be sorted
   */
  list: Ref<SortableItem[]>;

  /**
   * Enables/Disables drag and drop sorting
   * @default true
   */
  isEnabled?: boolean;

  /**
   * Class name for the ghost element
   * @default ''
   */
  ghostClass?: string;

  /**
   * Class name for the chosen element
   * @default ''
   */
  chosenClass?: string;

  /**
   * Sort the list in place
   * @default true
   */
  sortInPlace?: boolean;

  /**
   * Callback when the dragging starts
   */
  onDragStart?: (e: SortableOnDragStartEvent) => void;

  /**
   * Callback when the dragging ends
   */
  onDragEnd?: (e: SortableOnDragEndEvent) => void;
}

export interface SortableDragEvent extends DragEvent {
  /**
   * The old index of the dragged element
   */
  oldIndex: number;

  /**
   * The new index of the dragged element
   */
  newIndex: number;
}

export type SortableOnDragStartEvent = Omit<SortableDragEvent, 'newIndex'>;
export type SortableOnDragEndEvent = SortableDragEvent;

export interface UseSortableReturn {
  /**
   * Whether the element is currently being dragged
   */
  isDragging: Ref<boolean>;

  /**
   * The new position of the dragged element
   */
  newIndex: Ref<number>;

  /**
   * The original position of the dragged element
   */
  oldIndex: Ref<number>;
}