fix: handle dynamic selects with falsy select values (#9471)

when options are added later, we need to ensure the select value still stays in sync
fixes #9412
pull/9431/head
Simon H 1 year ago committed by GitHub
parent 19f84ca730
commit c1f6ee096d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: handle dynamic selects with falsy select values

@ -881,9 +881,10 @@ export function bind_window_size(type, update) {
const callback = () => update(window[type]); const callback = () => update(window[type]);
listen_to_events(window, ['resize'], callback); listen_to_events(window, ['resize'], callback);
} }
/** /**
* Finds the containing `<select>` element and potentially updates its `selected` state. * Finds the containing `<select>` element and potentially updates its `selected` state.
* @param {Element} dom * @param {HTMLOptionElement} dom
* @returns {void} * @returns {void}
*/ */
export function selected(dom) { export function selected(dom) {
@ -902,10 +903,15 @@ export function selected(dom) {
const select_value = select.__value; const select_value = select.__value;
// @ts-ignore // @ts-ignore
const option_value = dom.__value; const option_value = dom.__value;
// @ts-ignore const selected = select_value === option_value;
dom.selected = select_value === option_value; dom.selected = selected;
// @ts-ignore
dom.value = option_value; dom.value = option_value;
// Handle the edge case of new options being added to a select when its state is "nothing selected"
// and keeping the selection state in sync (the DOM auto-selects the first option on insert)
// @ts-ignore
if (select.__value === null) {
/** @type {HTMLSelectElement} */ (select).value = '';
}
} }
}); });
} }
@ -957,13 +963,16 @@ export function bind_select_value(dom, get_value, update) {
} }
update(value); update(value);
}); });
render_effect(() => { // Needs to be an effect, not a render_effect, so that in case of each loops the logic runs after the each block has updated
effect(() => {
const value = get_value(); const value = get_value();
if (value == null && !mounted) { if (value == null && !mounted) {
/** @type {HTMLOptionElement | null} */ /** @type {HTMLOptionElement | null} */
let selected_option = value === undefined ? dom.querySelector(':checked') : null; let selected_option = value === undefined ? dom.querySelector(':checked') : null;
if (selected_option === null) { if (selected_option === null) {
dom.value = ''; dom.value = '';
// @ts-ignore
dom.__value = null;
} }
const options = dom.querySelectorAll('option'); const options = dom.querySelectorAll('option');
for (const option of options) { for (const option of options) {

@ -0,0 +1,27 @@
import { ok, test } from '../../test';
export default test({
skip_if_ssr: 'permanent',
html: `
<p>selected: a</p>
<select>
<option disabled=''>x</option>
<option value="a">a</option>
<option value="b">b</option>
<option value="c">c</option>
</select>
`,
test({ assert, component, target }) {
assert.equal(component.selected, 'a');
const select = target.querySelector('select');
ok(select);
const options = [...target.querySelectorAll('option')];
// first enabled option should be selected
assert.equal(select.value, 'a');
assert.ok(options[1].selected);
}
});

@ -0,0 +1,12 @@
<script>
export let selected;
</script>
<p>selected: {selected}</p>
<select bind:value={selected}>
<option disabled>x</option>
{#each ["a", "b", "c"] as val}
<option>{val}</option>
{/each}
</select>

@ -0,0 +1,112 @@
import { ok, test } from '../../test';
// // same test as the previous one, but with a dynamic each block
export default test({
html: `
<p>selected: </p>
<select>
<option value="a">a</option>
<option value="b">b</option>
<option value="c">c</option>
</select>
<p>selected: </p>
`,
async test({ assert, component, target }) {
const select = target.querySelector('select');
ok(select);
const options = [...target.querySelectorAll('option')];
assert.equal(component.selected, null);
// no option should be selected since none of the options matches the bound value
assert.equal(select.value, '');
assert.equal(select.selectedIndex, -1);
assert.ok(!options[0].selected);
component.selected = 'a'; // first option should now be selected
assert.equal(select.value, 'a');
assert.ok(options[0].selected);
assert.htmlEqual(
target.innerHTML,
`
<p>selected: a</p>
<select>
<option value="a">a</option>
<option value="b">b</option>
<option value="c">c</option>
</select>
<p>selected: a</p>
`
);
component.selected = 'd'; // doesn't match an option
// now no option should be selected again
assert.equal(select.value, '');
assert.equal(select.selectedIndex, -1);
assert.ok(!options[0].selected);
assert.htmlEqual(
target.innerHTML,
`
<p>selected: d</p>
<select>
<option value="a">a</option>
<option value="b">b</option>
<option value="c">c</option>
</select>
<p>selected: d</p>
`
);
component.selected = 'b'; // second option should now be selected
assert.equal(select.value, 'b');
assert.ok(options[1].selected);
assert.htmlEqual(
target.innerHTML,
`
<p>selected: b</p>
<select>
<option value="a">a</option>
<option value="b">b</option>
<option value="c">c</option>
</select>
<p>selected: b</p>
`
);
component.selected = undefined; // also doesn't match an option
// now no option should be selected again
assert.equal(select.value, '');
assert.equal(select.selectedIndex, -1);
assert.ok(!options[0].selected);
assert.htmlEqual(
target.innerHTML,
`
<p>selected: </p>
<select>
<option value="a">a</option>
<option value="b">b</option>
<option value="c">c</option>
</select>
<p>selected: </p>
`
);
}
});

@ -0,0 +1,14 @@
<script>
// set as null so no option will be selected by default
export let selected = null;
</script>
<p>selected: {selected}</p>
<select bind:value={selected}>
{#each ['a', 'b', 'c'] as letter}
<option>{letter}</option>
{/each}
</select>
<p>selected: {selected}</p>
Loading…
Cancel
Save