Svelte input binding breaks when a reactive value is a reference type?

Viewed 3012

(I'm new to Svelte so it is quite likely that I'm doing something wrong here)

UPDATE: I've added a second, slightly different REPL which may demonstrate the problem better. Try this one: https://svelte.dev/repl/ad7a65894f8440ad9081102946472544?version=3.20.1


I've encountered a problem attempting to bind a text input to a reactive value.

I'm struggling to describe the problem in words, so hopefully a reduced demo of the issue in the attached REPL will make more sense.

https://svelte.dev/repl/6c8068ed4cc048919f71d87f9d020696?version=3.20.1

The demo contains two custom <Selector> components on a page.


The first component is passed two string values ("one" and "two"):

<Selector valueOne="one" valueTwo="two"/>

Clicking the buttons next to the input field sets selectedValue to one of these values.

This, in turn, triggers the following reactive declaration to update:

$: value = selectedValue

The input field is bound to this reactive value:

<input type="text" bind:value>

So clicking the "One" button sets the input text to "one", and clicking the "Two" button sets the input field to "two".

Importantly though, you can still type anything into the input field.


The second component is passed two array values:

<Selector valueOne={[1, "one"]} valueTwo={[2, "two"]}/>

Again, clicking the buttons sets selectedValue to one of these.

However this time the reactive declaration depends on an array element:

$: value = selectedValue[1]

Everything works as before, except now you can no longer type into the input field at all.

So the question is - why does <input bind:value> behave differently for these two:

$: value = aString

vs

$: value = anArray[x]
2 Answers

It seems that this is only an issue when using two-way bindings.

By switching to a one-way and an on:input handler, the problem goes away:

i.e. instead of this:

<input type="text" bind:value={valX}/>

use this:

<input type="text" value={valX} on:input={e => valX = e.target.value}/>

I'm pretty sure your reactive declaration is overwriting your bound value as soon as it changes, which is with every key stroke on the input and every button press. Meaning it technically is working, you're just reverting it each time it changes. Check out this version of it that uses a watcher.

Also binding to a reactive declaration means you're never actually changing the variables with the input (which you can see in your JSON result on the first selector when you type in the input the value doesn't update only on button click).

Why not lose the reactive declaration and bind directly to the variable you want. Then use an {#if} block to switch between which version of the input you're showing based on the truthiness of index?

<script>
    export let valueOne;
    export let valueTwo;
    export let index;

    let selectedValue = index? [] : '';
    let selectValue = (val) => selectedValue = val;
</script>
{#if index}
    <input type="text" bind:value={selectedValue[index]} placeholder="Type anything...">
{:else}
    <input type="text" bind:value={selectedValue} placeholder="Type anything...">
{/if}
<button on:click={() => selectValue(valueOne)}>One</button>
<button on:click={() => selectValue(valueTwo)}>Two</button>
<p>
    <strong>Selected value:</strong> {JSON.stringify(selectedValue)}
</p>

By binding directly to the selectedValue or an index of it you have the added benefit of changing the value with the input. Here's a working example in the REPL

Related