diff --git a/.changeset/tall-hounds-tap.md b/.changeset/tall-hounds-tap.md new file mode 100644 index 0000000000..3a9f7d79cb --- /dev/null +++ b/.changeset/tall-hounds-tap.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: support `defaultValue` on `` needs the options to exist before it can mark one + // as selected, so it is handled after the children, alongside `value` + if (node.name === 'select' && attribute.name === 'defaultValue') { + continue; + } + const name = get_attribute_name(node, attribute); if ( @@ -513,6 +519,23 @@ export function RegularElement(node, context) { } } + // deferred from the attribute loop above, so that the options it selects from + // have been created and had their values assigned + if (!has_spread && name === 'select') { + for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { + if (attribute.name === 'defaultValue') { + const { value, has_state } = build_attribute_value(attribute.value, context, (v, m) => + context.state.memoizer.add(v, m) + ); + + const update = b.stmt(b.call('$.set_default_select_value', node_id, value)); + + (has_state ? context.state.update : context.state.init).push(update); + break; + } + } + } + context.state.template.pop_element(); } diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js index 90c34d7035..2f312912bc 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/RegularElement.js @@ -46,7 +46,7 @@ export function RegularElement(node, context) { node.attributes.some( (attribute) => ((attribute.type === 'Attribute' || attribute.type === 'BindDirective') && - attribute.name === 'value') || + (attribute.name === 'value' || attribute.name === 'defaultValue')) || attribute.type === 'SpreadAttribute' ); const is_option_special = name === 'option'; diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 589685b5f2..083dfcbd4d 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -1,7 +1,7 @@ /** @import { Blocker, Effect } from '#client' */ import { DEV } from 'esm-env'; import { hydrating, set_hydrating } from '../hydration.js'; -import { get_descriptors, get_prototype_of } from '../../../shared/utils.js'; +import { get_descriptors, get_prototype_of, is_array } from '../../../shared/utils.js'; import { create_event, delegate, delegated, event, event_symbol } from './events.js'; import { add_form_reset_listener, autofocus } from './misc.js'; import * as w from '../../warnings.js'; @@ -26,7 +26,8 @@ import { set_class } from './class.js'; import { set_style } from './style.js'; import { ATTACHMENT_KEY, NAMESPACE_HTML, UNINITIALIZED } from '../../../../constants.js'; import { branch, destroy_effect, effect, managed } from '../../reactivity/effects.js'; -import { init_select, select_option } from './bindings/select.js'; +import { get_option_value, init_select, select_option } from './bindings/select.js'; +import { is } from '../../proxy.js'; import { flatten } from '../../reactivity/async.js'; export const CLASS = Symbol('class'); @@ -163,6 +164,51 @@ export function set_default_value(element, value) { element.value = existing_value; } +/** + * `` / `bind:value`. If it is absent, nothing has + // asked for a particular option yet, so the default selection should take effect — + // which is what the browser does with a `selected` attribute in the markup. + var has_explicit_value = '__value' in select; + var selected_index = select.selectedIndex; + + if (select.multiple) { + // a `multiple` select takes a list of values, so an option is part of the + // default selection when it is a member of that list — mirroring how + // `select_option` applies the current value + if (!is_array(value)) return; + + var selected = new Set([...select.selectedOptions]); + + for (var multi_option of select.options) { + set_selected(multi_option, value.includes(get_option_value(multi_option))); + } + + if (has_explicit_value) { + for (var option_to_restore of select.options) { + option_to_restore.selected = selected.has(option_to_restore); + } + } + + return; + } + + for (var option of select.options) { + set_selected(option, is(get_option_value(option), value)); + } + + if (has_explicit_value) { + select.selectedIndex = selected_index; + } +} + /** * @param {Element} element * @param {string} attribute @@ -559,7 +605,16 @@ export function attribute_effect( var select = /** @type {HTMLSelectElement} */ (element); effect(() => { - select_option(select, /** @type {Record} */ (prev).value, true); + var attrs = /** @type {Record} */ (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); }); } diff --git a/packages/svelte/src/internal/client/dom/elements/bindings/select.js b/packages/svelte/src/internal/client/dom/elements/bindings/select.js index e390e23ea0..44d50a7c7e 100644 --- a/packages/svelte/src/internal/client/dom/elements/bindings/select.js +++ b/packages/svelte/src/internal/client/dom/elements/bindings/select.js @@ -156,7 +156,7 @@ export function bind_select_value(select, get, set = get) { } /** @param {HTMLOptionElement} option */ -function get_option_value(option) { +export function get_option_value(option) { // __value only exists if the