Search code examples
sveltesortablejs

Svelte and sortablejs : issues with dynamic list


I have a similar problem as the one shown in this Svelte REPL.

If I create a sortablejs object in a Svelte project from a dynamic list (that means a list created with {#each...} logic blocks from a dynamic variable - stages in the example), the job is done regarding the stages variable (objects are moved correctly in the array) but the display is not correct after the move.

My guess is :

  • sortablejs attaches tags to an item of the list foo when it's created
  • moving the order on the HTML list triggers the move on the stages array as expected
  • items order in the HTML list change because this foo list is bind to the stages array
  • as the order changed in this HTML list, the sortablejs tags order changes as well.

It's like the move happened once in the stages array and twice in the foo list.

What I expect is that, after a move, the order of the foo HTML list would be the same as the order of the stages array.

I didn't find how to solve this issue. Any help/hind appreciated! Thank you!

EDIT

I thought the REPL was releavant enough to explain my problem but it was not. The key point of this issue is not the use of {#each...} but the fact that stages is dynamic.

Here is the code I working with :

<script lang="ts">
  import Sortable from "sortablejs"
  import ChipExo from "./ChipExo.svelte"
  import { exercicesParams, moveExercice } from "../store"
  import { onMount } from "svelte"

  let listIdsForChips: string[] = []
  $: {
    listIdsForChips = []
    for (const ex of $exercicesParams) {
      listIdsForChips.push(ex.id ?? ex.uuid)
    }
    listIdsForChips = listIdsForChips
  }
  let chipsList: HTMLDivElement
  onMount(() => {
    chipsList = document.getElementById("chips-list")
    const sortable = Sortable.create(chipsList, {
      animation: 150,
      onEnd: (evt) => {
        exercicesParams.update((l) => {
          return moveExercice(l, evt.oldIndex, evt.newIndex)
        })
      },
    })
  })
</script>

<div
  class="w-full grid justify-items-stretch place-content-stretch grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-7 xl:grid-cols-8 2xl:grid-cols-10 gap-2 p-0 items-center overflow-x-auto whitespace-nowrap"
  id="chips-list"
>
  {#each listIdsForChips as id, indice (indice)}
    <ChipExo text={id} {indice} />
  {/each}
</div>

and here is the display in HTML

Chips List

The listIdsForChips variable is reset every time the exercicesParams store changes. For example, Chips can be removed by clicking on the close icon of the chip. So the listIdsForChips variable has to be dynamic.

I hope this edit will clarify the issue.


Solution

  • In the first example stages doesn't depend on any other value, so just let stages = ... without reactive declaration

    When adding a key to the #each block it seems to be working correctly - tutorial

    {#each stages as stage (stage.data)}
      <li>{stage.data}</li>
    {/each}
    

    In your second example, instead of setting the index as key

    {#each listIdsForChips as id, indice (indice)}
        <ChipExo text={id} {indice} />
    {/each}
    

    since you map to the id or uuid take the value instead

    {#each listIdsForChips as id, indice (id)}
        <ChipExo text={id} {indice} />
    {/each}
    

    or if there's no unique field - from the tutorial explanation

    You can use any object as the key, as Svelte uses a Map internally — in other words you could do (thing) instead of (thing.id). Using a string or number is generally safer, however, since it means identity persists without referential equality, for example when updating with fresh data from an API server.

    The second problem with the reactive statements seems to be with the first line - changing to a second variable name seems to be one fix

     let listIdsForChips: string[] = []
      $: {
        let lIFC = []
        for (const ex of $exercicesParams) {
          lIFC.push(ex.id ?? ex.uuid)
        }
        listIdsForChips = lIFC
      }
    

    To reduce unpredicted behaviour and make the dependencies clearer, wrapping in a function is an alternative way

    let listIdsForChips: string[] = []
    
    $: updateListIdsForChips($exercicesParams)
    
    function updateListIdsForChips(eP) {
        let lIFC = []
        for (const ex of eP) {
          lIFC.push(ex.id ?? ex.uuid)
        }
        listIdsForChips = lIFC
    }
    

    or in this case reducing to one line

     let listIdsForChips: string[] = []
     $: listIdsForChips = $exercicesParams.map(p => p.id ?? p.uuid)
    

    I updated the REPL