svelte-pragmatic-list
v0.0.7
Published
Opinionated simple and headless DND list reordering action for Svelte. The goal here is to provide a simple and easy way to build a DND list reordering in Svelte. This a single action to put on the the list container to allow every child to be draggable a
Downloads
1,229
Readme
Svelte-pragmatic-list
Opinionated simple and headless DND list reordering action for Svelte. The goal here is to provide a simple and easy way to build a DND list reordering in Svelte. This a single action to put on the the list container to allow every child to be draggable and reorderable.
Svelte-pragmatic-list uses pragmatic-dnd as DND engine which is known to be fast and lightweight.
It makes no assumptions about the structure of the list items neither the style of the list. You can use any shape of list items and style them as you wish.
Features
- Simple and easy to use
- No assumptions about the list items
- No assumptions about the style of the list
- Nested list / board
- Horizontal or vertical list
- Drag handle
- Auto scroll on window / list edge
- Drag indicator
- Custom drag preview
- Cross window DND
- Move / Copy / (Swap) modes
- Svelte version agnostic
Installation
npm install svelte-pragmatic-list
pnpm add svelte-pragmatic-list
bun add svelte-pragmatic-list
Usage
The most basic example is the following. Just declare the list of items and use the dnd
action on the container of the list. The action needs pass the items and declare the onChange callback to update them. Every child of the container will be draggable and reorderable by default.
<script>
import { dnd } from 'svelte-pragmatic-list';
let items = $state(['item 1', 'item 2', 'item 3', 'item 4', 'item 5']);
// Or without the state rune if you are in Svelte 4
// let items = ['item 1', 'item 2', 'item 3', 'item 4', 'item 5'];
</script>
<div
use:dnd={{
items,
onChange: (newItems) => (items = newItems)
}}
>
{#each items as item}
<div>
{item}
</div>
{/each}
</div>
Using a drag handle
If you want to use a drag handle, just add a data-dnd-handle
attribute to the element you want to use as a handle inside the draggable child.
- the handle must be a direct child of the draggable element.
<script>
import { dnd } from 'svelte-pragmatic-list';
let items = $state(['item 1', 'item 2', 'item 3', 'item 4', 'item 5']);
</script>
<div
use:dnd={{
items,
onChange: (newItems) => (items = newItems)
}}
>
{#each items as item}
<div>
<div data-dnd-handle>:::</div>
{item}
</div>
{/each}
</div>
Displaying a drag indicator
If you want to display a drag indicator, just add a data-dnd-indicator
attribute to the element you want to use as an indicator. The indicator will be displayed at the place between the two items where the dragged item will be dropped. Its position will be automatically updated when the dragged item is moved.
- The indicator must be a direct child of the container.
- The indicator must have an absolute position and the container must have any position. (svelte-pragmatic-list will add these styles for you)
- You should add the hidden attribute to it in order to hide it when the list is mounted.
<script>
import { dnd } from 'svelte-pragmatic-list';
let items = $state(['item 1', 'item 2', 'item 3', 'item 4', 'item 5']);
</script>
<div
use:dnd={{
items,
onChange: (newItems) => (items = newItems)
}}
>
{#each items as item}
<div>
{item}
</div>
{/each}
<div data-dnd-indicator hidden style="height: 2px; background-color: red; width: 100%;"></div>
</div>
Nested list / board
You can also use svelte-pragmatic-list to create a nested list or a board. To do so just nest the dnd list inside your markup and use the type
and accept
options of the action.
For example, you can create a board with column and cards. The columns will accept ["column"]
and have a type of column
and the cards will accept ["card"]
and have a type of card
. This will prevent a card for being dropped in a column and a column to be dropped in a card.
<script>
import { dnd } from 'svelte-pragmatic-list';
let board = $state([
{
title: 'Column 1',
cards: ['Card 1', 'Card 2', 'Card 3']
},
{
title: 'Column 2',
cards: ['Card 4', 'Card 5', 'Card 6']
},
{
title: 'Column 3',
cards: ['Card 7', 'Card 8', 'Card 9']
}
]);
</script>
<div
use:dnd={{
getItems: () => board,
onChange: (newBoard) => (board = newBoard),
type: 'column',
accept: ['column']
}}
>
{#each board as column}
<div>
<h2>{column.title}</h2>
<div
use:dnd={{
getItems: () => column.cards,
onChange: (newCards) => (column.cards = newCards),
type: 'card',
accept: ['card']
}}
>
{#each column.cards as card}
<div>
{card}
</div>
{/each}
</div>
</div>
{/each}
</div>
Customizing the drag preview
You can customize the drag preview with the preview options.
- it is possible to transform and translate the default preview element with the
y
andx
scale
androtate
options. - it is also possible to create a custom preview element by using an element with the
data-dnd-preview
attribute. You can get the data of the item being dragged with getPreviewData function. - Use the hidden attribute to hide the default preview element.
- The preview element must be a direct child of the container.
<script>
import { dnd } from 'svelte-pragmatic-list';
let items = $state(['item 1', 'item 2', 'item 3', 'item 4', 'item 5']);
let previewData = $state(null);
</script>
<div
use:dnd={{
items,
onChange: (newItems) => (items = newItems),
preview: {
y: 10,
x: 10,
scale: 1.1,
rotate: 5,
getPreviewData: (item) => (previewData = item)
}
}}
>
{#each items as item}
<div>
{item}
</div>
{/each}
<div data-dnd-preview hidden>
{previewData}
</div>
</div>
Drag modes and dynamic drag mode
You can use the mode
option to set the drag mode. The available drag modes are move
, copy
or swap
. The default mode is move
.
You can also use a dynamic drag mode by using a function that returns the mode. This function will receive the item being dragged, the DOM element, and the isSameList
boolean as arguments. It can be useful to change the mode based on the item being dragged or the target list or based on a key being pressed.
<script>
import { dnd } from 'svelte-pragmatic-list';
let items = $state(['item 1', 'item 2', 'item 3', 'item 4', 'item 5']);
</script>
<div
use:dnd={{
items,
onChange: (newItems) => (items = newItems),
mode: 'copy'
// Or with a dynamic mode
// mode: (item, element, isSameList) => {
// return isSameList ? 'move' : 'copy';
// }
}}
>
{#each items as item}
<div>
{item}
</div>
{/each}
</div>
Cross window DND (WIP)
You can use svelte-pragmatic-list to create a DND between two tabs or from a parent to an iframe. To do so, set externalDrag
to the list that can be dragged outside of the window and externalDrop
to the list that can receive the dragged item. It is possible to use all drag modes. Svelte-pragmatic-list will take care of the communication between the windows using localStorage. For example in case a swap between the two windows both state will be updated.
<!-- routes/+page.svelte -->
<script>
import { dnd } from 'svelte-pragmatic-list';
let draggableItems = $state(['item 1', 'item 2', 'item 3', 'item 4', 'item 5']);
let droppableItems = $state(['item 6', 'item 7', 'item 8', 'item 9', 'item 10']);
</script>
<div
use:dnd={{
items: draggableItems,
onChange: (newItems) => (draggableItems = newItems),
externalDrag: true
}}
>
{#each draggableItems as item}
<div>
{item}
</div>
{/each}
</div>
<div
use:dnd={{
items: droppableItems,
onChange: (newItems) => (droppableItems = newItems),
externalDrop: true
}}
>
{#each droppableItems as item}
<div>
{item}
</div>
{/each}
</div>
<!-- The second list of the iframe will be able to receive the elements of the first list. -->
<!-- The second list of the parent window will be able to receive the elements of the first list from the iframe -->
<iframe src="/" />