I am building an application where users can add and delete items of different "types". For example there may be a text type where users can edit the text, and an image type where users can drop in an image.
Adding an item to the list will cause the list to update and display all old items along with the new item. Similarly, deleting an item will remove that item and cause the list to only display the remaining items.
The add functionality works perfectly fine. The issue es with deletes when the items are of different types. Here is my (simplified) code:
// List.svelte
<script>
import {writable} from "svelte/store";
import Wrapper from "./Wrapper.svelte";
const items = [
{
type: "text",
value: "Test 1"
},
{
type: "img",
value: ".gif"
}
];
const listStore = writable(items);
</script>
{#each $listStore as item, i}
<Wrapper item={item} store={listStore} myIndex={i} />
{/each}
// Wrapper.svelte
<script>
import {onMount} from "svelte";
export let item;
export let store;
export let myIndex;
let DynamicItem;
const handleDeleteItem = () => {
store.update((items) => ([...items.slice(0, myIndex), ...items.slice(myIndex + 1)]))
}
onMount(async () => {
let imported;
if(item.type === "text") {
imported = await import("./TextItem.svelte")
} else {
imported = await import("./ImgItem.svelte")
}
DynamicItem = imported.default;
})
</script>
<svelte:ponent this={DynamicItem} data={item} />
<button on:click={handleDeleteItem}>Delete</button>
// TextItem.svelte
<script>
export let data;
</script>
<div contenteditable="true" bind:textContent={data.value} />
// ImgItem.svelte
<script>
export let data;
</script>
<img src="{data.value}" width="100px" height="100px" alt="img alt" />
When the user deletes the TextItem
from the list by clicking on the first delete button
, the ImgItem
no longer displays the image and instead only renders the src
attribute as text.
When looking at the HTML output, before the delete action it looks like this:
<body>
<div contenteditable="true">Test 1</div>
<button>Delete</button>
<img src=".gif" alt="img alt" width="100px" height="100px">
<button>Delete</button>
</body>
And afterwards, it looks like this:
<body>
<div contenteditable="true">.gif</div>
<button>Delete</button>
</body>
So it looks like Svelte is not truly deleting the TextItem
but moving the content from the next item into its place.
How can I fix my code so that items are truly deleted? The goal should be to produce the following HTML by performing the same action:
<body>
<img src=".gif" alt="img alt" width="100px" height="100px">
<button>Delete</button>
</body>
Any help is appreciated. Thank you
I am building an application where users can add and delete items of different "types". For example there may be a text type where users can edit the text, and an image type where users can drop in an image.
Adding an item to the list will cause the list to update and display all old items along with the new item. Similarly, deleting an item will remove that item and cause the list to only display the remaining items.
The add functionality works perfectly fine. The issue es with deletes when the items are of different types. Here is my (simplified) code:
// List.svelte
<script>
import {writable} from "svelte/store";
import Wrapper from "./Wrapper.svelte";
const items = [
{
type: "text",
value: "Test 1"
},
{
type: "img",
value: "https://media.giphy./media/KzW9EkUE6NJrwqG0UX/giphy.gif"
}
];
const listStore = writable(items);
</script>
{#each $listStore as item, i}
<Wrapper item={item} store={listStore} myIndex={i} />
{/each}
// Wrapper.svelte
<script>
import {onMount} from "svelte";
export let item;
export let store;
export let myIndex;
let DynamicItem;
const handleDeleteItem = () => {
store.update((items) => ([...items.slice(0, myIndex), ...items.slice(myIndex + 1)]))
}
onMount(async () => {
let imported;
if(item.type === "text") {
imported = await import("./TextItem.svelte")
} else {
imported = await import("./ImgItem.svelte")
}
DynamicItem = imported.default;
})
</script>
<svelte:ponent this={DynamicItem} data={item} />
<button on:click={handleDeleteItem}>Delete</button>
// TextItem.svelte
<script>
export let data;
</script>
<div contenteditable="true" bind:textContent={data.value} />
// ImgItem.svelte
<script>
export let data;
</script>
<img src="{data.value}" width="100px" height="100px" alt="img alt" />
When the user deletes the TextItem
from the list by clicking on the first delete button
, the ImgItem
no longer displays the image and instead only renders the src
attribute as text.
When looking at the HTML output, before the delete action it looks like this:
<body>
<div contenteditable="true">Test 1</div>
<button>Delete</button>
<img src="https://media.giphy./media/KzW9EkUE6NJrwqG0UX/giphy.gif" alt="img alt" width="100px" height="100px">
<button>Delete</button>
</body>
And afterwards, it looks like this:
<body>
<div contenteditable="true">https://media.giphy./media/KzW9EkUE6NJrwqG0UX/giphy.gif</div>
<button>Delete</button>
</body>
So it looks like Svelte is not truly deleting the TextItem
but moving the content from the next item into its place.
How can I fix my code so that items are truly deleted? The goal should be to produce the following HTML by performing the same action:
<body>
<img src="https://media.giphy./media/KzW9EkUE6NJrwqG0UX/giphy.gif" alt="img alt" width="100px" height="100px">
<button>Delete</button>
</body>
Any help is appreciated. Thank you
Share Improve this question edited Nov 28, 2019 at 7:40 Hitmands 14.2k4 gold badges40 silver badges77 bronze badges asked Nov 28, 2019 at 1:53 mrstack999mrstack999 3273 silver badges12 bronze badges1 Answer
Reset to default 11So it looks like Svelte is not truly deleting the TextItem but moving the content from the next item into its place.
Yes, absolutely, this is what is happening.
Svelte (like React) only recreate an element in a given position when its nature has obviously changed, for example if the tag name is not the same. Otherwise, it is considered that it is the same element / ponent with just some props / state that has changed.
In order to hint the framework that the identity of an element has changed, you must the key
functionality. In React, it's a literal key
prop. In Svelte, keys are only supported in {#each ...}
blocks (for now?). The syntax is the following (docs):
{#each expression as name, index (key)}...{/each}
# also works, I think it is an oversight in the docs currently
{#each expression as name (key)}...{/each}
The value of the key
must be unique to each item for it to work. Typically you'd use an id of sort. But truly, it can be anything. If the value change for the element / ponent at a given position, then it will be recreated.
So, in your case, you could fix your issue by using the item type as a key, for example:
{#each $listStore as item, i (item.type)}
<Wrapper item={item} store={listStore} myIndex={i} />
{/each}
This fixes the issue you're asking about, but it's not such a great idea I must say. For other reasons, you'd better add real unique ids to your items and use them instead. With the above code, items of the same type will continue to share DOM elements, and that's probably not what you want. You may have issues with focus or internal state (value) of DOM elements like inputs, for example.
So, something like this is better and is probably the way to go:
<script>
import {writable} from "svelte/store";
import Wrapper from "./Wrapper.svelte";
const items = [
{
id: 1,
type: "text",
value: "Test 1"
},
{
id: 2,
type: "img",
value: "https://media.giphy./media/KzW9EkUE6NJrwqG0UX/giphy.gif"
}
];
const listStore = writable(items);
</script>
{#each $listStore as item, i (item.id)}
<Wrapper item={item} store={listStore} myIndex={i} />
{/each}