fix: apply `<select defaultValue>` when the element is spread

The client only handled `defaultValue` in the non-spread path, so
`<select {...props}>` compiled to `attribute_effect` and never reached
`set_default_select_value`. `set_attributes` can't apply it either,
since assigning the property does nothing on a select. It now marks the
default option from the same effect that handles `value`, after the
options exist.

Server-side the key was only read as `defaultvalue`. That is what the
compiler emits for an attribute written in the template, but a spread
passes keys through as the user wrote them, so `defaultValue` was
ignored and SSR disagreed with the client. Both spellings are accepted
now.

This is the case #18447 was filed for: `<select {...form.fields.x.as('select')}>`
in SvelteKit is always a spread.
pull/18591/head
Socialpranker 5 days ago
parent 5cfa5495a0
commit 22c07e69bc

@ -584,7 +584,16 @@ export function attribute_effect(
var select = /** @type {HTMLSelectElement} */ (element);
effect(() => {
select_option(select, /** @type {Record<string | symbol, any>} */ (prev).value, true);
var attrs = /** @type {Record<string | symbol, any>} */ (prev);
// `defaultValue` is meaningless as a property here, so `set_attributes` cannot
// apply it. Mark the default option once the options exist, as we do for the
// non-spread case.
if ('defaultValue' in attrs) {
set_default_select_value(select, attrs.defaultValue);
}
select_option(select, attrs.value, true);
init_select(select);
});
}

@ -332,14 +332,16 @@ export class Renderer {
* @returns {void}
*/
select(attrs, fn, css_hash, classes, styles, flags, is_rich) {
// the attribute name is lowercased by the compiler, as with any HTML attribute
const { value, defaultvalue, ...select_attrs } = attrs;
// the compiler lowercases attribute names written in the template, but a spread
// passes the keys through as the user wrote them, so accept both spellings
const { value, defaultValue, defaultvalue, ...select_attrs } = attrs;
const default_value = defaultValue === undefined ? defaultvalue : defaultValue;
this.push(`<select${attributes(select_attrs, css_hash, classes, styles, flags)}>`);
this.child((renderer) => {
// `<select>` has no defaultValue attribute — it only says which option is
// selected by default, so it applies when there is no `value` to override it
renderer.local.select_value = value === undefined ? defaultvalue : value;
renderer.local.select_value = value === undefined ? default_value : value;
fn(renderer);
});
this.push(`${is_rich ? '<!>' : ''}</select>`);

@ -0,0 +1,56 @@
import { test } from '../../test';
import { flushSync } from 'svelte';
export default test({
async test({ assert, target }) {
/**
* @param {NodeListOf<any>} options
* @param {any[]} selected
*/
function check_options(options, selected) {
for (let i = 0; i < options.length; i++) {
assert.equal(options[i].selected, selected[i], `option ${i}`);
}
}
/**
* @param {HTMLOptionElement} option
*/
function select_option(option) {
option.selected = true;
option.dispatchEvent(new Event('change', { bubbles: true }));
}
const reset = /** @type {HTMLInputElement} */ (target.querySelector('input[type=reset]'));
const [test1, test2] = target.querySelectorAll('select');
const [test1_span] = target.querySelectorAll('span');
// a spread carrying defaultValue selects the matching option
{
const options = test1.querySelectorAll('option');
check_options(options, [false, true, false]);
assert.htmlEqual(test1_span.innerHTML, 'b');
}
// value in the same spread still wins over defaultValue
{
const options = test2.querySelectorAll('option');
check_options(options, [false, false, true]);
}
// changing the selection and resetting goes back to defaultValue
select_option(test1.querySelectorAll('option')[2]);
select_option(test2.querySelectorAll('option')[0]);
flushSync();
assert.htmlEqual(test1_span.innerHTML, 'c');
reset.click();
await Promise.resolve();
flushSync();
check_options(test1.querySelectorAll('option'), [false, true, false]);
check_options(test2.querySelectorAll('option'), [false, true, false]);
assert.htmlEqual(test1_span.innerHTML, 'b');
}
});

@ -0,0 +1,26 @@
<script>
let props1 = $state({ defaultValue: 'b' });
let props2 = $state({ defaultValue: 'b', value: 'c' });
let selected = $state();
</script>
<form>
<!-- spread carrying defaultValue, no value -->
<select {...props1} bind:value={selected}>
<option value="a">A</option>
<option value="b">B</option>
<option value="c">C</option>
</select>
<!-- spread carrying both, value wins -->
<select {...props2}>
<option value="a">A</option>
<option value="b">B</option>
<option value="c">C</option>
</select>
<input type="reset" value="Reset" />
</form>
<p><span class="test-1">{selected}</span></p>

@ -0,0 +1,12 @@
<select>
<option value="a">A</option>
<option selected="" value="b">B</option>
</select>
<select>
<option value="a">A</option>
<option selected="" value="b">B</option>
</select>
<select>
<option selected="" value="a">A</option>
<option value="b">B</option>
</select>

@ -0,0 +1,18 @@
<script>
const props = { defaultValue: 'b' };
</script>
<select defaultValue="b">
<option value="a">A</option>
<option value="b">B</option>
</select>
<select {...props}>
<option value="a">A</option>
<option value="b">B</option>
</select>
<select defaultValue="b" value="a">
<option value="a">A</option>
<option value="b">B</option>
</select>
Loading…
Cancel
Save