How can I correctly pre-select a multiple select input in Svelte with objects?

In this sample component...

<script>

  let availableValues = [
    {id: 1, name: "one"},
    {id: 2, name: "two"},
    {id: 3, name: "three"},
    {id: 4, name: "four"}
  ]

  let selectedValues = [
    {id: 1, name: "one"},
    {id: 2, name: "two"}
  ]

</script>

<select multiple bind:value={selectedValues}>
  {#each availableValues as value}
    <option value={value}>
      {value.name}
    </option>
  {/each}
</select>

values are correctly selected and bound to selectedValues.

Nonetheless, when loading the component for the first time, the selectedValues are not reflected in the select input - in other words, they're not bound because the select input doesn't seem to have a way of determining whether the selectedValues match any of the availableValues. Nonetheless, when selecting (clicking) on multiple values, the selectedValues array gets populated as expected.

In other frameworks, it's possible to pass in a function that helps "derive" the selected value, something like (foo) => foo.id == value.id to get around the ambiguity of object equality. In Svelte there's a similar alternative, using the selected attribute, but which doesn't seem to neatly solve this particular scenario.

What is the best way to handle a similar situation in Svelte?

Going further, What is a sane way of dealing with this when the initial selectedValues also happens to be an externally-bound prop (i.e. <MyMultipleSelect bind:selectedValues={vals} />)?


Solution 1:

This is because javascript objects equality is based on reference (= memory location) and not on value. As a quick and enlightening experiment, open your browser console and evaluate { a: 'foo', b: 0 } === { a: 'foo', b: 0 }. Yep, false, because these two objects, while equal in value, are in fact distinct entities stored at different addresses in memory.

So how do you get around that when evaluating pre-selected options in a multiple select input? Well, you use scalars for your option value attributes, because scalars equality is based on value.

Sherif's solution is one way to do it (javascript strings, which is what you get when you obtain the JSON representation of an object, are in fact scalars), but considering your objects have an id attribute, which are assumedly unique identifiers, I would use that instead:

<script>
  let availableValues = [
    {id: 1, name: "one"},
    {id: 2, name: "two"},
    {id: 3, name: "three"},
    {id: 4, name: "four"}
  ]

  let selectedIds = [1, 2]

  $: selectedValues = availableValues.filter((v) => selectedIds.includes(v.id))
  $: console.log(selectedValues)
</script>

<select multiple bind:value={selectedIds}>
  {#each availableValues as value}
    <option value={value.id}>
      {value.name}
    </option>
  {/each}
</select>

Edit: selected values as externally bound prop

The principle remains the same, but instead of having your selectedIds set from within the component, they would be passed on as a prop (along with, supposedly, the overall set of options). So something like this:

// App.svelte
<script>
  import { onMount } from 'svelte'
  import MySelect from './MySelect.svelte'
  let options = []
  let selectedIds = []

  onMount(async () => {
    // set of options fetched from wherever (API, store, local/session storage, etc.), returns an array of objects similar to your 'availableValues' (each object MUST have an 'id' attribute)
    options = await getOptions()
    // currently selected values fetched from wherever (API, store, local/session storage, etc.), returns an array of ids (integers, UUID strings, etc., whatever matches the 'id' attribute of your options array)
    selectedIds = await getCurrent()
  }

  $: selectedValues = options.filter((o) => selectedIds.includes(o.id))
</script>

<MySelect {options} bind:selectedIds={selectedIds} />
<ul>
  {#each selectedValues as sv (sv.id)}
    <li>{sv.name} [id: {sv.id}]</li>
  {/each}
</ul>

//MySelect.svelte
<script>
  export let options = []   // or hardcoded default values
  export let selectedIds = []  // or hardcoded default values
</script>

<select multiple bind:value={selectedIds}>
    {#each options as option}
        <option value={option.id}>
            {option.name}
        </option>
    {/each}
</select>