Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Svelte 5: <select>/<option> elements get incorrect/inconsistent values when applying undefined with spread or reactive value #11616

Open
sheijne opened this issue May 14, 2024 · 3 comments
Milestone

Comments

@sheijne
Copy link

sheijne commented May 14, 2024

Describe the bug

<select> and <option> elements are getting incorrect/inconsistent value attribute when the value is nullish when using dynamic assignment of the value attribute or when using object spread.

It seems to be due to how nullish values are applied to the <option> element. I'm running into the following scenario's:

  1. <option value={undefined}> renders with an empty value attribute.
  2. <option {value}> with a non-reactive variable renders with an empty value attribute.
  3. <option {value}> with a reactive variable renders without a value attribute.
  4. <option {...value}> or <option {...{value}}> with a non-reactive variable renders without value attribute.
  5. <option {...value}> or <option {...{value}}> with a reactive variable renders without value attribute.

Which makes it impossible to set the value of an <option> to undefined or null in specific scenarios, since the value prop of an HTMLOptionElement fallback to the label when no value attribute is provided.

This is causing troubles when using <select bind:value> with a default value (nullish), on initial mount the value will be nullish, once mounted the value is updated to the label of whatever the associated <option> has.

I have not tested this, but I would assume similar scenarios might apply to checkbox and radio inputs.

Reproduction

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAE7WST0vEMBDFv0oIHnahtoe91WxBEI8e9Gg9ZJupG8gmJZkWJfS7S5p122X_aAWPmfdm-L1HPK2lAkfzV0813wHN6X3T0ITiZxMergOFQBPqTGurMGGusrLBotQlVkY7JB1XLTiyJjcOOcLCB6nEWn6AyEmrBdRSg0jiWBttgVcoOzgVLyuTNddY4GcuX9f75V2pR-jJPbIercGDCpBMxH2ssM-yMb1m21URs7Nsu4ojIbviMQSPreTEv6CV-n0RjelQyrJnWTCWmjlQUCHZSC3ywbL2U2c_1MxMg9JosjccYPviAWreKmRZdBQD4XByxHky-vaQpuNW8o06BZv08Su8if8s5JH-G8znHxFn8V2Fm0d2VOD357pcX3TMLTFuHdP6NE39_h9N-5xZ6AXkP_D-AyxN6M4IWUsQNEfbQv_WfwEXGNU4lQQAAA==

Logs

No response

System Info

System:
    OS: macOS 14.4.1
    CPU: (10) arm64 Apple M1 Pro
    Memory: 370.41 MB / 32.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 20.11.1 - ~/.nvm/versions/node/v20.11.1/bin/node
    npm: 10.2.4 - ~/.nvm/versions/node/v20.11.1/bin/npm
    pnpm: 8.15.6 - ~/.nvm/versions/node/v20.11.1/bin/pnpm
    bun: 1.1.4 - ~/.bun/bin/bun
  Browsers:
    Chrome: 124.0.6367.202
    Edge: 124.0.2478.97
    Safari: 17.4.1

Severity

annoyance

@sheijne sheijne changed the title Svelte 5: select/options get incorrect/inconsistent values when applying undefined with prop or spread Svelte 5: select/option elements get incorrect/inconsistent values when applying undefined with prop or spread May 14, 2024
@sheijne sheijne changed the title Svelte 5: select/option elements get incorrect/inconsistent values when applying undefined with prop or spread Svelte 5: <select>/<option> elements get incorrect/inconsistent values when applying undefined with prop or spread May 14, 2024
@sheijne sheijne changed the title Svelte 5: <select>/<option> elements get incorrect/inconsistent values when applying undefined with prop or spread Svelte 5: <select>/<option> elements get incorrect/inconsistent values when applying undefined with spread or reactive value May 14, 2024
@dummdidumm
Copy link
Member

dummdidumm commented May 21, 2024

The problem here is that nullish values (undefined and null) are generally mean "remove the attribute", which is what happens in this case for the spread cases. For the reactive variable case it's the "has this value changed" check which results to false because the initial value of the cache variable is undefined. Using null instead fixes the issues except for the spread case.

What's the use case for setting the value of an option to undefined?

Svelte 4 has the same problem for spread, works in the other cases though.

@dummdidumm dummdidumm added this to the 5.0 milestone May 21, 2024
@sheijne
Copy link
Author

sheijne commented May 21, 2024

What's the use case for setting the value of an option to undefined?

I do this in order to set a variable to undefined using two-way binding. The main reason I ran into this was because of the following scenario:

<script>
  let { intent = 'primary', children } = $props();
</script>

<button class="flex ..." class:bg-primary={intent === 'primary'}>
  {@render children}
</button>

Somewhere in a docs app there was a preview that uses a select element to switch between the different prop values, something like:

<script>
  import { Button } from '...';

  let intent;
</script>

<select bind:value={intent}>
  <option value={undefined}>Default</option>
  <option value="primary">Primary</option>
</select>

<Button {intent}>Button</Button>

The option with value={undefined} is used to mimic the default behavior; what happens when no prop is passed. This worked "fine", however, I'm replacing the select and option elements with components, and that's when I started running into problems. I found out that using undefined as a value was no longer possible, because an option without value will fall back to using the content as value, <option>Default</option> results in option.value === 'Default'.

Using null instead fixes the issues except for the spread case.

After reading this I went back to try it again, and to my confusion it wasn't working, and when I tried to reproduce it in the REPL it was working. In my application the select element was suddenly "empty" on page load, and the value of the variable was null, selecting the Default option set the variable to Default (the same is true when the value is undefined). Finally I figured out what the issue was, and why it isn't working in my app. It turns out that adding any kind of spread, even an empty object results in the attribute being removed, and on initial mount the value will remain null, but the option has a different value, resulting in the option not being selected. You can see a reproduction in the following REPL:

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAE22NwWrDMAyGX8WIHVoIyd1zDYM9QGGwy7yDmyjD4CjGlgPF-N2Hk0N76E3S__36CszOYwL5U4DsgiDhIwTogO-hLWlDzwgdpDXHsV1UGqMLrA0Z9shisz6juIi3xJbxRNn787shNTw4UpPb9HcDpShfHB39nfbeuaqhZTuU0OPI4uZoknu6O9Qa2K10eC6l_a-i9H1fatWfONvsWQ0H9KJgIES32Hg3oK_H9ESr4ZBqQ9DBsk5udjiB5Jix_tZ_w4IDDh4BAAA=

I also started playing around with inputs with bind:group, and it produces a similar result, only in that case the value ends up being undefined instead of null (to add: when using undefined as value it works fine, which makes sense since it does not have a fallback value like option elements):

https://svelte-5-preview.vercel.app/#H4sIAAAAAAAAE5WQwWrDMBBEf0UsOSRg4p5d2VDaL2iOdg6OvUlFFUlYq9Ai_O9F6zimgRR6lGZ25u1GOCqNHoo6gmnPCAW8OAcZ0LdLD39BTQgZeBuGLv1I3w3KUdWYhjSSuLQ6oCjFylNLuDZB683zLA5tr-wjsfvA7vNgvxa9ToY9O0xDK2W8w47WXJFNYdltLNlkvtAY2atLtUON3ZWqEHFHgzKnKWEzyjxZ2Osn30GZvmCVF5LWkbJGxO12G8dxiiljwhqrNzy2QZPMJ1PF9Ryz1L_zwnftDL60S90eUHPfNZKrlXGBRLp72QCPNHADYc7TYIMrI2u_2UTONLfgCeZ1PvAdz3zB-mn_P6p58AHYLP_BBhmcba-OCnsoaAg47scfMt1ZgYQCAAA=

Personally I get why nullish attributes are removed, it makes sense in most cases to provide a convenience API. Though it seems that the "technically correct" way to represent a value attribute with a nullish value would be an empty attribute. At least it seems more in line with how HTML and the DOM work natively. (edit: on second thought I believe what I'm saying here is not correct)

My main concern isn't what is "technically" correct, but rather the inconsistent behaviour between different scenarios, especially the scenario in the REPLs above seems odd (I would guess that's a bug?). The inconsistency between non-reactive values, reactive values, and spread makes things a bit confusing, at least for me.

@sheijne
Copy link
Author

sheijne commented May 21, 2024

I'm currently working around this by using an empty string as a value:

<script>
  import { Button } from '...';

  let intent = "";
</script>

<select bind:value={intent}>
  <option value="">Default</option>
  <option value="primary">Primary</option>
</select>

<Button intent={intent || undefined}>Button</Button>

In my opinion not the prettiest, but it works for my specific use case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants