How to ensure local-only transitions when Svelte is reusing the parent dom element
Asked Answered
A

1

4

In Svelte, I have a component which is used to display items in two different lists. When those items are moved from one list to the other, they use a transition to animate in or out.

However, I also have a way to filter what is displayed on the screen. Displaying a new set of items would use the same component, but with different data. In this case, I don't want the transition animation to occur. I assumed that adding the local modifier would do the trick, but it seems that Svelte isn't dropping the parent element to the list, but instead reusing it and adding the new data in the existing list DOM element.

I've tried to reproduce what I'm seeing in the sample code below.

Wanted Behavior:

  1. Clicking on a TODO will toggle the TODO from one list to the other.
  2. Clicking "Switch Categories" will switch which TODOs are listed, without animating the <li>s of the TODOs that are added or removed.

Actual Behavior:

  1. Happens as expected.
  2. The todos that are switched in do so with the animation.

How can I change my example so that I get the effect that I want?

App.svelte:

<script>
    import Todos from './Todos.svelte';

    let todos = [
        { id: 1, category: 'personal', name: 'Walk dog', done: false },
        { id: 2, category: 'personal', name: 'Take out trash', done: false },
        { id: 3, category: 'work', name: 'Make login page functional', done: false },
        { id: 4, category: 'work', name: 'Make login page elegant', done: false }
    ];

    let currentCategory = 'personal';
    const toggleCategory = () => {
        currentCategory = currentCategory == 'personal' ? 'work' : 'personal';
    }

    const toggleTodo = id => {
        todos = todos.map(todo => {
            if (todo.id === id) {
                return { ...todo, done: !todo.done }
            }
            return todo;
        });
    }

    $: categoryTodos = todos.filter(x => x.category == currentCategory);
</script>

<button on:click={toggleCategory}>Switch Categories</button>
<Todos todos={categoryTodos} {toggleTodo}>
</Todos>

Todos.svelte:

<script>
    import { slide } from 'svelte/transition';
    export let todos;
    export let toggleTodo;

    $: complete = todos.filter(t => t.done);
    $: incomplete = todos.filter(t => !t.done);
</script>

<h1>Incomplete</h1>
<ul>
    {#each incomplete as {id, name} (id)}
    <li transition:slide|local on:click={() => toggleTodo(id)}>{name}</li>
    {/each}
</ul>
<h1>Complete</h1>
<ul>
    {#each complete as {id, name} (id)}
    <li transition:slide|local on:click={() => toggleTodo(id)}>{name}</li>
    {/each}
</ul>
Assailant answered 1/10, 2019 at 15:6 Comment(0)
K
6

Update

This key feature is now part of Svelte since v3.28.0 (see issue).

The syntax is the follwing:

{#key expression}...{/key}

Previous answer

In React, you would use the key prop to make the renderer recreates an element that could have been reused (same tag, etc.).

// React
<Todos items={items} key={currentCategory} />

But Svelte doesn't support key, does it? Well, somewhat. Svelte does have an equivalent feature, but only in {#each ...} blocks.

The syntax is this (docs -- this precise syntax is not mentioned in the docs, but I guess it has just been forgotten):

{#each expression as name (key)}...{/each}

Like in React, the component will be recreated when the value of the key changes (and reused otherwise).

And soooo...

<script>
  export let allItems
  export let currentCategory

  $: items = allItems.filter(x => x.category === currentCategory)
</script>

{#each [items] as todos (currentCategory)}
  <Todos {todos} />
{/each}

Hu hu. Right?

Using currentCategory as a key will create a new <Todos /> component each time the cattegory changes, which is probably what you want in your case.

Like in React, the value of the key must be chosen wisely to recreate every time is needed, but not more (or it would kill the desired inter-item transition in your case).

The value of the key is not limited to the currently evaluated item in the each loop. It can come from anywhere in the scope in Svelte, so you can get creative. It could even be an inline object {} which would recreate... Well, basically all the time!

Edit

You can turn the hack into its own component, for cleaner syntax in the consumers:

<!-- Keyed.svelte -->

<script>
    export let key
    // we just need a 1-length array
    const items = [0]
</script>

{#each items as x (key)}
    <slot />
{/each}

Use like this:

<script>
  import Keyed from './Keyed.svelte'
  
  export let items
  export let category
</script>

<Keyed key={category}>
  <Todos {items} />
</Keyed>

See example in Svelte's REPL.

Kapellmeister answered 26/11, 2019 at 9:16 Comment(2)
Someone has created an issue to request this feature: github.com/sveltejs/svelte/issues/3994Kapellmeister
I published a component implementing this workaround on npm: github.com/rixo/svelte-keyKapellmeister

© 2022 - 2024 — McMap. All rights reserved.