javascriptsveltesortablejs

svelte and sortable js, how to handle a dynamic array


I'm new to svelte and am working on a simple drag and drop list using sortablejs as the base. I load the data from a nested array of objects to instantiate the lists, and want any changes from sortable to be replicated to the array, but I can't get it to stop behaving oddly.

I binded the array to the components so that they can send the Sortable order to update the array. The array seems to update correctly, but the actual list becomes very janky: double moving items and undoing moves. I can't wrap my head around what exactly is causing this.

REPL

App.svelte

<script>    
    import List from "./List.svelte";
    let items = [
        [
            {id: 1,name: "one"},
            {id: 2,name: "two"},
        ],
        [
            {id: 3,name: "three"},
            {id: 4,name: "four"},
        ]
    ]
</script>

{#each items as category, i}
    <h2>Category {i}</h2>
    <ul>
        <List bind:fullArr={items} index={i}>
            {#each category as item}
                <li data-id={item.id} >{item.name}</li>
            {/each}
        </List>
    </ul>
{/each}

{JSON.stringify(items)}

List.svelte

<script>
        import Sortable from 'sortablejs';
        import { onMount } from 'svelte';

        export let fullArr;
        export let index;

        let arr = fullArr[index];
    
        let list;
        let sortable;
        onMount(() => {
            sortable = new Sortable(list, {
                group: "list",  
                onSort: onSort,
            });
        })
        function onSort(){
            
            const order = sortable.toArray() 
            fullArr[index] = order.map(id => {
                
                //id is searched from full array to account for item going between lists
                return fullArr.flat().find(item => item.id == id)
            })
        }


</script>

<div bind:this={list}>
    <slot></slot>
</div>

There is this similar question, but the solution seems to only apply to a singular list and not a group of lists


Solution

  • As in the other question, the #each block is missing the key - tutorial

    {#each category as item (item.id)}
    

    Instead of the List component, that adds more complexity than benefit, all logic could be handled inside the main component using an action - docs - tutorial

    REPL

    <script>
        import Sortable from 'sortablejs';
        
        let items = [
            [
                {id: 1,name: "one"},
                {id: 2,name: "two"},
            ],
            [
                {id: 3,name: "three"},
                {id: 4,name: "four"},
            ]
        ]
    
        function initSortable(list, index) {
            const sortable = new Sortable(list, {
                group: "list",  
                onSort() {
                    const order = sortable.toArray()
                    const allItems = items.flat()
                    items[index] = order.map(id => {                                
                        return allItems.find(item => item.id == id)
                    })
                },
            });
        }
    </script>
    
    {#each items as category, i}
        <h2>Category {i}</h2>
        <ul use:initSortable={i} >      
            {#each category as item (item.id)}
                <li data-id={item.id} >{item.name}</li>
            {/each}     
        </ul>
    {/each}
    
    {JSON.stringify(items)}
    

    And a different way to update items using the sortable onEnd event

    REPL

    <script>
        import Sortable from 'sortablejs';
    
        let items = [
            [
                {id: 1,name: "one"},
                {id: 2,name: "two"},
            ],
            [
                {id: 3,name: "three"},
                {id: 4,name: "four"},
            ]
        ]
    
        function initSortable(list) {
            const sortable = new Sortable(list, {
                group: "list",  
                onEnd(event) {
                    const fromListIndex = event.from.dataset.listIndex
                    const toListIndex = event.to.dataset.listIndex
                    const movedItem = items[fromListIndex].splice(event.oldIndex, 1)[0]
                    items[toListIndex].splice(event.newIndex, 0, movedItem)
                    items = items // important to make the UI update to the changes made to items via .splice method
                },
            });
        }
    </script>
    
    {#each items as category, i}
        <h2>Category {i}</h2>
        <ul data-list-index={i} use:initSortable >      
            {#each category as item (item.id)}
                <li data-id={item.id} >{item.name}</li>
            {/each}     
        </ul>
    {/each}
    
    {JSON.stringify(items)}