João Pedro
João Pedro

Reputation: 978

After removing element and adding a new one in an array in svelte store gets added an empty slot

I have a very simple SvelteKit (1.0) app, you can check the repo here : https://github.com/joaopedrocoelho/sveltecounter , and deployed with Amplify at https://main.d2u56jyg3xz1fe.amplifyapp.com/

whenever I add a counter after deleting another one the nameList array in my svelte store an empty element is added in the array, I fixed it by filtering the array but I'm wondering what's the root of the problem, I made a video of the issue:

https://streamable.com/op8op3

+page.svelte

<script lang="ts">
  import Counter from '../components/Counter.svelte';
  import { sum, nameList } from '../store';
  let counters = [Counter];
  let currentSum: number;
  let currentNameList: string[] = [];

  sum.subscribe((value) => {
    currentSum = value;
  });


//this is where the empty element is first logged
  nameList.subscribe((value) => {
    console.log('nameList', value);
    currentNameList = value.filter(Boolean);
  });

  const addCounter = () => {
    counters = [...counters, Counter];
  };
</script>

<div class="flex m-auto flex-col max-w-sm">
  <h1 class="text-6xl text-center mb-6">Multiple Counter</h1>
  {#each counters as Counter, i}
    <Counter index={i} />
  {/each}
  <button
    on:click={addCounter}
    class="max-w-sm w-full mt-2 m-auto text-center
 bg-green-400 rounded text-white cursor-pointer"
  >
    new counter
  </button>
  <p class="flex ">title list: {currentNameList}</p>
  <p class="flex">sum of count: {currentSum}</p>
</div>

Counter.svelte

<script lang="ts">
  import { onMount, onDestroy } from 'svelte';
  import { sum, nameList } from '../store';
  export let index: number = 0;
  let count: number = 0;
  let name: string = 'new';
  let counterRef: HTMLDivElement;

  const increment = () => {
    sum.update((n) => n + 1);
    count++;
  };

  const decrement = () => {
    if (count > 0) {
      sum.update((n) => n - 1);
      count--;
    }
  };

  const reset = () => {
    sum.update((n) => n - count);
    count = 0;
  };

  const removeCounter = () => {
    sum.update((n) => n - count);
    counterRef.remove();
    nameList.update((n) => {
      let newList = [...n];
      newList.splice(index, 1);
      return newList;
    });
  };

  onMount(() => {
    nameList.update((n) => {
      let newList = [...n];
      newList[index] = name;
      return newList;
    });
  });

  onDestroy(() => {
    nameList.update((n) => {
      let newList = [...n];
      newList.splice(index, 1);
      return newList;
    });
  });

  const onChangeList = (event: Event) => {
    nameList.update((n) => {
      let newList = [...n];
      newList[index] = (event.target as HTMLInputElement).value;

      return newList;
    });
  };
</script>

<div
  class="max-w-sm bg-gray-100
    shadow-lg m-auto 
    flex relative 
    items-center mb-4 py-2"
  bind:this={counterRef}
>
  <input value={name} on:input={onChangeList} class="text-gray-600 mx-4 px-1 w-32" />
  <span class="text-lg font-bold px-4">{count}</span>
  <div class="ml-auto flex">
    <button on:click={increment} class="btn bg-red-500 rounded-l">+</button>
    <button on:click={decrement} class="btn bg-blue-500">-</button>
    <button on:click={reset} class="btn bg-yellow-500 rounded-r">0</button>
    <button
      on:click={removeCounter}
      class="bg-white text-gray-600 
        rounded-full h-8 w-8 mx-4
        flex items-center justify-center font-bold px-1 py-0">X</button
    >
  </div>
</div>

<style>
  .btn {
    --text-opacity: 1;
    color: #fff;
    color: rgba(255, 255, 255, var(--text-opacity));
    padding-top: 0.25rem;
    padding-bottom: 0.25rem;
    padding-left: 0.75rem;
    padding-right: 0.75rem;
    font-size: 1.125rem;
  }
</style>

Upvotes: 0

Views: 111

Answers (1)

Corrl
Corrl

Reputation: 7721

The problem is that when a counter is removed, its count is substracted from the sum, the counterRef removed from the Dom (not recommended) and the corresponding entry removed from nameList - but the counters the #each block is built upon stays the same.
So after what you show in the video - adding two, removing one, adding another - the index of the lastly added element is one more than expected and hence the empty entry REPL with logs

set,subscribe and update are usually not necessary inside a component because the $ store prefix can be used instead

Here's a different approach REPL

  • using a custom store for the counters (tutorial)
  • using derived stores for calculating sum and namesList (docs-tutorial)
  • using bind:property (tutorial) to write values of name and count to the corresponding object in counters (values will be initialized with default values set inside component export let count= 0)
  • using createEventDispatcher (docs-tutorial) to trigger removal of a counter
App.svelte
<script>
    import Counter from './Counter.svelte';
    import { counters, nameList, sum } from './store';
</script>

<h1>Multiple Counter</h1>
    {#each $counters as c, i (c)}
    <Counter bind:name={c.name}
                     bind:count={c.count}
                     on:remove="{() => counters.remove(c)}"
                     />
    {/each}
<button on:click={counters.add} >
    new counter
</button>

<pre>
title list: {$nameList.join(', ')}
sum of count: {$sum}

counters: {JSON.stringify($counters, null, 2)}
</pre>
Counter.svelte
<script>
    import { onMount, onDestroy } from 'svelte';
    import { createEventDispatcher } from 'svelte';

    const dispatch = createEventDispatcher();

    export let count = 0;
    export let name = 'new';

    const increment = () => count++

    const decrement = () => {
        if (count > 0) {
            count--;
        }
    };

    const reset = () => count = 0
    
</script>

<div>
    <input bind:value={name} />
    <span>{count}</span>
    <div>
        <button on:click={increment}>+</button>
        <button on:click={decrement} disabled={count === 0}>-</button>
        <button on:click={reset}>0</button>
        <button on:click={() => dispatch('remove')} >X</button>
    </div>
</div>
store.js
import {writable, derived} from 'svelte/store'

function createCountersStore() {

    const { subscribe, set, update } = writable([{}]);

    return {
        subscribe,
        set,
        add() {
            update(value => [...value, {}])
        },
        remove(counter) {
            update(value => value.filter(c => c !== counter))
        }
    };
}

export const counters = createCountersStore()

export const nameList = derived(counters, $counters => $counters.map(counter => counter.name))

export const sum = derived(counters, $counters => {
    return $counters.reduce((sum, counter) => sum += counter.count, 0 )
})

Upvotes: 3

Related Questions