Reactivity issue, Svelte, Radio button

Viewed 423

My case is that I have multiple objects in an array. The user should (via radio button) select which item should be used.

However, the user shall also be able to change the name of object (obj.name).

The value shall be stored elsewhere, but it must be the obj.name of the selected object.

Here is a sample code (works out of the box in Svelte REPL):

<script>
    let selectedName = "";
    let arr = [];
    let obj1 = {};
    obj1.id = 1;
    obj1.name = "One";
    let obj2 = {};
    obj2.id = 2;
    obj2.name = "Two";
    let obj3 = {};
    obj3.id = 3;
    obj3.name = "Three";

    arr.push(obj1);
    arr.push(obj2);
    arr.push(obj3);
</script>

<input type="text" bind:value={selectedName}/>
<hr>
{#each arr as row}
    <input type="radio" bind:group={selectedName} value={row.name}/> {row.id}
    <input type="text" bind:value={row.name}/>
    <br/>
{/each}

In this example, all renders nicely, you can select 1, 2 or 3, and the textbox above gets updated with the name of the object ("One", "Two" or "Three").

If you select 2 and then change the name of 1 from "One" to "Uno", and then select 1 - the value is changed to "Uno" in the textbox above. BUT! If you now change the text from "Uno" to "Ein" - the textbox doesn't update.

The natural thing would be to have the radio button bind the value, like this:

    <input type="radio" bind:group={selectedName} bind:value={row.name}/> {row.id}

... but then everything just gets messed up completely! :-)

Is there anyone that can shine a light over this, I'd be very happy!

And to clarify; the solution of storing the id instead of name is not the solution to my problem. Above example is just a simplified version to illustrate the issue.

3 Answers

I got an approach that works by displaying a reactive variable, called name, in the text input. I defined the reactive variable to be arr[selectedIndex].name, and selectedIndex is the value associated with selecting a radio button.

Code that works out of the box in the Svelte REPL:

<script>
    let arr = [
        {
            id: 1,
            name: "One"
        },
        {
            id: 2,
            name: "Two"
        },
        {
            id: 3,
            name: "Three"
        }
    ];
    let selectedIndex;
    let name = "";
    $: {
        if (selectedIndex !== undefined) {
            name = arr[selectedIndex].name;
        }
    }
</script>

<input type="text" value={name} />
<hr />
{#each arr as row, index}
    <input type="radio" bind:group={selectedIndex} value={index} />
    {row.id}
    <input type="text" bind:value={row.name}/>
    <br/>
{/each}

When adding a log of the radio button on text input change, it can be seen that the value of the radio button changes accordingly even without adding the bind:value >> REPL

<script>    
    let arr = [
        { id: 1, name: "One" },
        { id: 2, name: "Two" },
        { id: 3, name: "Three" }
    ];
        let selectedName = "";
    
    function checkRadioValue(event) {
        console.log(document.getElementById(event.target.dataset.id))
    }
</script>

<input type="text" bind:value={selectedName}/>
<hr>
{#each arr as row}
    <input type="radio" bind:group={selectedName} value={row.name} id={row.id}/> {row.id}
    <input type="text" bind:value={row.name} on:input={checkRadioValue} data-id={row.id}/>
    <br/>
{/each}

What doesn't happen is the update of the bind:group value. This only seems to be triggered on a change event of the radio buttons, looks like this is "the main problem".

Here's another example using bind:group with radio buttons and checkboxes >> REPL
Click on the button down below to change both array contents -> the displayed values get instantly updated (no bind:value needed for that), but the values saved in selectedScoop/selectedFlavours displayed above do not. Only after changing a radio button or check mark directly the values react to modifications.

So a possible way to achieve the desired functionality (alternatively to the reactive approach via the id/index) would be to handle the focus and input event on the input fields >> REPL

<script>
    let arr = [
        { id: 1, name: "One" },
        { id: 2, name: "Two" },
        { id: 3, name: "Three"}
    ];
    let selectedName    

    function handleTextInputEvents(event) {
        selectedName = event.target.value
    }
</script>

{selectedName}

<hr />
{#each arr as row, index (row.id)}
    <input type="radio" name="names" bind:group={selectedName} value={row.name}/>
    {row.id}
    <input type="text" bind:value={row.name} on:focus={handleTextInputEvents} on:input={handleTextInputEvents}/>
    <br/>
{/each}

In relation to my comment to your question, here is a workaround using a tailored event handler on the row input box that will update the checked option (in addition to updating unchecked options):

<input type="text" value={row.name} on:input={(e) => {
  const { target } = e
  row.name = target.value
  const radio = target.previousElementSibling
  if (radio.checked) {
    selectedName = target.value
  }
}} />

Demo REPL

Related