Search code examples
javascriptsveltesvelte-3

Svelte : code in dynamic statement doesn't update components


I have three components : App, Keyboard and Key.

Keyboard component loads multiple Key components based on a variable "keys" with such structure : { char: string, isPressed: boolean }[] Key component has a boolean pressed prop that is used among other things for styling.

I'd like to be able to reset all those isPressed properties from the App component and get the view updated accordingly when some keys are pressed. With Vue.js I might have used a global event, here I tried with binded props and dynamic statements. So I have a dynamic statement in Keyboard component that executes when I want but works only until you press another key. The funny thing is that it's working fine when I use the "clear" button.

I made a repl here : https://svelte.dev/repl/b7c11d9f0e494195884be0994387ba4c?version=3.35.0

App

<main>
  <Keyboard on:pressed-change="{checkKeys}"
            bind:clear={clear} />
</main>

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

let clear = false;
const text = 'AC'; 

function checkKeys({ detail: keys }) {
    console.log('check keys')
  if (keys.sort().join('') === text) {
        console.log('clear')
    clear = true;
  }
}
</script>

Keyboard

<main>
  <div>
    <section class="keys">
      {#each keys as { char, isPressed }, i}
        <Key char="{char}"
             bind:pressed={isPressed}
             on:key-press={handleKeyPress} />
      {/each}
    </section>

    <section>
      { pressed.join(' | ') }
    </section>
    <button on:click={clearKeyboard}>
      clear
    </button>
  </div>
</main>

<script>
import { createEventDispatcher } from 'svelte';
import Key from './Key.svelte';

export let clear;
let pressed = [];

const dispatch = createEventDispatcher();
    
let keys = [
  { char: "A", isPressed: false },
  { char: "B", isPressed: false },
  { char: "C", isPressed: false },
  { char: "D", isPressed: false },
  { char: "E", isPressed: false },
  { char: "F", isPressed: false },
  { char: "G", isPressed: false },
];

function handleKeyPress({ detail: char }) {
  const keyIndex = pressed.indexOf(char)
  if (keyIndex >= 0) {
    const tempArr = [...pressed];
    tempArr.splice(keyIndex, 1);
    pressed = tempArr;
    return;
  }
  pressed = [...pressed, char];
}

function clearKeyboard() {
  console.log('clear keyboard');
  keys = keys.map(key => ({ ...key, isPressed: false }));
  pressed = [];
}

$: if (clear) {
  clearKeyboard()
  clear = false;
}
$: dispatch('pressed-change', pressed);
</script>

Key

<div class="key"
     class:pressed
     on:click="{ press }">
  { char }
</div>

<script>
import { createEventDispatcher } from 'svelte';

export let char;
export let pressed;

const dispatch = createEventDispatcher();

function press() {
  pressed = !pressed;
  dispatch('key-press', char);
}
</script>

Thanks for the help.


Solution

  • The issue you are having is caused by some race condition somewhere.

    Svelte does not entirely 'instantaneously' update the DOM but rather schedules changes in batches. What is happening in your code is that the code setting the key to be pressed is conflicting with a similar signal 'unsetting' it.

    You can tell Svelte to explicitly wait until after the currently scheduled changes have been applied by using await tick() (the tick is when Svelte renders to the DOM)

    import { tick } from 'svelte'
    
    async function clearKeyboard() {
      await tick()
      console.log('clear keyboard');
      keys = keys.map(key => ({ ...key, isPressed: false }));
      pressed = [];
    }
    

    In the code above, first the current changes will be applied, then the keys are all set to false.

    You can visualize what is happening if you add a delay in the mix, by adding the following line just after await tick()

    await new Promise(res => setTimeout(res, 2000)) // will pause execution for two seconds