svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Svelte 5 flip behavior

Open jessecoleman opened this issue 1 year ago • 3 comments

Describe the bug

After updating my project from svelte 4 to 5 ("5.0.0-next.264"), the behavior for svelte's animate:flip action has changed subtly. I'm using the built in animation and transition actions in tandem with @neodrag/svelte but I think the issue is in the way svelte 5 reconciles changes to {#each} blocks.

What I expect to happen is the flip animation plays after I release the element I'm dragging. In svelte 5, the flip animation doesn't trigger and it abruptly snaps back to its start position.

Reproduction

Minimal reproduction: https://github.com/jessecoleman/svelte-5-flip

v4: https://github.com/user-attachments/assets/316cc2df-bdc3-4b0d-ac1f-76778aaad8de

v5: https://github.com/user-attachments/assets/a5f72ed8-b146-43fd-9d08-12f02c78f22f

Logs

No response

System Info

System:
    OS: Linux 5.15 Ubuntu 22.04.3 LTS 22.04.3 LTS (Jammy Jellyfish)
    CPU: (22) x64 Intel(R) Core(TM) Ultra 7 155H
    Memory: 12.14 GB / 15.31 GB
    Container: Yes
    Shell: 5.8.1 - /usr/bin/zsh
  Binaries:
    Node: 20.11.1 - ~/.nvm/versions/node/v20.11.1/bin/node
    npm: 10.2.4 - ~/.nvm/versions/node/v20.11.1/bin/npm
    pnpm: 8.15.5 - ~/.local/share/pnpm/pnpm
  npmPackages:
    svelte: 5.0.0-next.264 => 5.0.0-next.264

Severity

annoyance

jessecoleman avatar Oct 13 '24 17:10 jessecoleman

As per docs, it shouldn't work (unless I misunderstood something):

An animation is triggered when the contents of a keyed each block are re-ordered. Animations do not run when an element is added or removed, only when the index of an existing data item within the each block changes. Animate directives must be on an element that is an immediate child of a keyed each block.

But if you swap items, animation will work:

<svelte:options runes={true} />

<script>
  import { draggable } from "@neodrag/svelte";
  import { flip } from "svelte/animate";

  let list = $state([0, 1, 2, 3]);
  let dragId = $state(-1);
  let dragPos = $state([0, 0]);

  const handleDragStart = (e, id) => {
    dragPos = [e.detail.offsetX, e.detail.offsetY];
    dragId = id;
  };

  const handleDrag = (e) => {
    dragPos = [e.detail.offsetX, e.detail.offsetY];
  };

  const handleDragEnd = (e) => {
    // NOTE: this swapping triggers animate
    let item = list[0]
    list[0] = list[1]
    list[1] = item

    dragPos = [0, 0];
    dragId = -1;
  };
</script>

<div class="list">
  {#each list as li (li)}
    <div
      class="list-item"
      style="
        transform: translate3d(0px, {40 * li}px);
      "
      animate:flip
      use:draggable={{
        position:
          dragId === li
            ? { x: dragPos[0], y: dragPos[1] }
            : { x: 0, y: 40 * li },
      }}
      on:neodrag:start={(e) => handleDragStart(e, li)}
      on:neodrag={handleDrag}
      on:neodrag:end={handleDragEnd}
    >
      {li}
    </div>
  {/each}
</div>


<style>
  .list {
    position: relative;
  }

  .list-item {
    position: absolute;
    width: 35px;
    height: 35px;
    background: red;
  }
</style>

PS: The code from the repo does work in Svelte 4 (one will have to downgrade to 4.2.19, to make it work). Was it a bug or a feature?

someone2080 avatar Oct 13 '24 20:10 someone2080

Yeah, the swap behavior is working for me, I mostly wanted to avoid having to hack together some imperative animation to achieve the "return to origin" effect that was default in Svelte 4. I'm guessing there are some performance optimizations in Svelte 5 that take advantage of the list order remaining the same. Is there a best practice for firing off an animation imperatively in Svelte (maybe using WAAPI?) I already have a reference to the DOM element in the dragend event.

BTW, here's the full non-toy sample of the application where I'm relying on this behavior: https://gramjam.app/classic

jessecoleman avatar Oct 13 '24 21:10 jessecoleman

I was able to work around this by conditionally adding a css class with transition: transform 0.5s ease to the element being dragged after releasing. It works well enough, although it creates a bit more complexity in my application code. Ok with closing this one for now.

jessecoleman avatar Oct 29 '24 15:10 jessecoleman