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:
<li>
s of the TODOs that are added or removed.Actual Behavior:
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>
This key feature is now part of Svelte since v3.28.0 (see issue).
The syntax is the follwing:
{#key expression}...{/key}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With