From 558a3c963bca4423cdd7b93d9588d05546dfa629 Mon Sep 17 00:00:00 2001 From: dai Date: Thu, 11 Sep 2025 03:10:45 +0200 Subject: [PATCH] fix: transform input defaults from spread (#16481) * fix: transform input defaults from spread * chore: add changeset * fix: prevent duplicates * do not remove defaults if they are in spreads * fix spreading twice * tweak * tweak * drive-by: remove unused export * tweak * undo comment change, to minimise diff * oops * tweak --------- Co-authored-by: 7nik Co-authored-by: Rich Harris --- .changeset/quiet-planes-doubt.md | 5 ++++ .../client/visitors/RegularElement.js | 18 +++++++++-- .../client/visitors/shared/element.js | 5 +++- .../server/visitors/shared/element.js | 3 ++ packages/svelte/src/constants.js | 1 + .../client/dom/elements/attributes.js | 30 +++++++++++++++++-- packages/svelte/src/internal/client/index.js | 1 - packages/svelte/src/internal/server/index.js | 11 ++++++- .../form-default-value-from-spread/_config.js | 10 +++++++ .../main.svelte | 8 +++++ 10 files changed, 85 insertions(+), 7 deletions(-) create mode 100644 .changeset/quiet-planes-doubt.md create mode 100644 packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/_config.js create mode 100644 packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/main.svelte diff --git a/.changeset/quiet-planes-doubt.md b/.changeset/quiet-planes-doubt.md new file mode 100644 index 0000000000..bd895f00ef --- /dev/null +++ b/.changeset/quiet-planes-doubt.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: transform input defaults from spread diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js index 4296aa959e..e906061650 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js @@ -72,6 +72,7 @@ export function RegularElement(node, context) { let has_spread = node.metadata.has_spread; let has_use = false; + let should_remove_defaults = false; for (const attribute of node.attributes) { switch (attribute.type) { @@ -172,7 +173,12 @@ export function RegularElement(node, context) { bindings.has('group') || (!bindings.has('group') && has_value_attribute)) ) { - context.state.init.push(b.stmt(b.call('$.remove_input_defaults', context.state.node))); + if (has_spread) { + // remove_input_defaults will be called inside set_attributes + should_remove_defaults = true; + } else { + context.state.init.push(b.stmt(b.call('$.remove_input_defaults', context.state.node))); + } } } @@ -202,7 +208,15 @@ export function RegularElement(node, context) { bindings.has('checked'); if (has_spread) { - build_attribute_effect(attributes, class_directives, style_directives, context, node, node_id); + build_attribute_effect( + attributes, + class_directives, + style_directives, + context, + node, + node_id, + should_remove_defaults + ); } else { for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) { if (is_event_attribute(attribute)) { diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js index 9143a57025..4b32dab82a 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js @@ -16,6 +16,7 @@ import { build_expression, build_template_chunk, Memoizer } from './utils.js'; * @param {ComponentContext} context * @param {AST.RegularElement | AST.SvelteElement} element * @param {Identifier} element_id + * @param {boolean} [should_remove_defaults] */ export function build_attribute_effect( attributes, @@ -23,7 +24,8 @@ export function build_attribute_effect( style_directives, context, element, - element_id + element_id, + should_remove_defaults = false ) { /** @type {ObjectExpression['properties']} */ const values = []; @@ -91,6 +93,7 @@ export function build_attribute_effect( element.metadata.scoped && context.state.analysis.css.hash !== '' && b.literal(context.state.analysis.css.hash), + should_remove_defaults && b.true, is_ignored(element, 'hydration_attribute_changed') && b.true ) ) diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js index 7207564ef9..84692fca9c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js @@ -11,6 +11,7 @@ import { import { regex_starts_with_newline } from '../../../../patterns.js'; import * as b from '#compiler/builders'; import { + ELEMENT_IS_INPUT, ELEMENT_IS_NAMESPACED, ELEMENT_PRESERVE_ATTRIBUTE_CASE } from '../../../../../../constants.js'; @@ -401,6 +402,8 @@ function build_element_spread_attributes( flags |= ELEMENT_IS_NAMESPACED | ELEMENT_PRESERVE_ATTRIBUTE_CASE; } else if (is_custom_element_node(element)) { flags |= ELEMENT_PRESERVE_ATTRIBUTE_CASE; + } else if (element.type === 'RegularElement' && element.name === 'input') { + flags |= ELEMENT_IS_INPUT; } const object = build_spread_object(element, attributes, context); diff --git a/packages/svelte/src/constants.js b/packages/svelte/src/constants.js index 69cd213940..63324c860f 100644 --- a/packages/svelte/src/constants.js +++ b/packages/svelte/src/constants.js @@ -28,6 +28,7 @@ export const HYDRATION_ERROR = {}; export const ELEMENT_IS_NAMESPACED = 1; export const ELEMENT_PRESERVE_ATTRIBUTE_CASE = 1 << 1; +export const ELEMENT_IS_INPUT = 1 << 2; export const UNINITIALIZED = Symbol(); diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index a5b7140f25..fb6a92cc82 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -268,10 +268,27 @@ export function set_custom_element_data(node, prop, value) { * @param {Record | undefined} prev * @param {Record} next New attributes - this function mutates this object * @param {string} [css_hash] + * @param {boolean} [should_remove_defaults] * @param {boolean} [skip_warning] * @returns {Record} */ -export function set_attributes(element, prev, next, css_hash, skip_warning = false) { +function set_attributes( + element, + prev, + next, + css_hash, + should_remove_defaults = false, + skip_warning = false +) { + if (hydrating && should_remove_defaults && element.tagName === 'INPUT') { + var input = /** @type {HTMLInputElement} */ (element); + var attribute = input.type === 'checkbox' ? 'defaultChecked' : 'defaultValue'; + + if (!(attribute in next)) { + remove_input_defaults(input); + } + } + var attributes = get_attributes(element); var is_custom_element = attributes[IS_CUSTOM_ELEMENT]; @@ -467,6 +484,7 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal * @param {Array<() => any>} sync * @param {Array<() => Promise>} async * @param {string} [css_hash] + * @param {boolean} [should_remove_defaults] * @param {boolean} [skip_warning] */ export function attribute_effect( @@ -475,6 +493,7 @@ export function attribute_effect( sync = [], async = [], css_hash, + should_remove_defaults = false, skip_warning = false ) { flatten(sync, async, (values) => { @@ -490,7 +509,14 @@ export function attribute_effect( block(() => { var next = fn(...values.map(get)); /** @type {Record} */ - var current = set_attributes(element, prev, next, css_hash, skip_warning); + var current = set_attributes( + element, + prev, + next, + css_hash, + should_remove_defaults, + skip_warning + ); if (inited && is_select && 'value' in next) { select_option(/** @type {HTMLSelectElement} */ (element), next.value); diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index c5b7bb845c..dbff5c4599 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -28,7 +28,6 @@ export { attach } from './dom/elements/attachments.js'; export { remove_input_defaults, set_attribute, - set_attributes, attribute_effect, set_custom_element_data, set_xlink_attribute, diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 62ee22d6fc..3aa44f2daa 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -8,7 +8,8 @@ import { subscribe_to_store } from '../../store/utils.js'; import { UNINITIALIZED, ELEMENT_PRESERVE_ATTRIBUTE_CASE, - ELEMENT_IS_NAMESPACED + ELEMENT_IS_NAMESPACED, + ELEMENT_IS_INPUT } from '../../constants.js'; import { escape_html } from '../../escaping.js'; import { DEV } from 'esm-env'; @@ -187,6 +188,7 @@ export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) { const is_html = (flags & ELEMENT_IS_NAMESPACED) === 0; const lowercase = (flags & ELEMENT_PRESERVE_ATTRIBUTE_CASE) === 0; + const is_input = (flags & ELEMENT_IS_INPUT) !== 0; for (name in attrs) { // omit functions, internal svelte properties and invalid attribute names @@ -200,6 +202,13 @@ export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) { name = name.toLowerCase(); } + if (is_input) { + if (name === 'defaultvalue' || name === 'defaultchecked') { + name = name === 'defaultvalue' ? 'value' : 'checked'; + if (attrs[name]) continue; + } + } + attr_str += attr(name, value, is_html && is_boolean_attribute(name)); } diff --git a/packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/_config.js b/packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/_config.js new file mode 100644 index 0000000000..3808ae6530 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/_config.js @@ -0,0 +1,10 @@ +import { test } from '../../test'; + +export default test({ + mode: ['server'], + html: ` + + + +` +}); diff --git a/packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/main.svelte b/packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/main.svelte new file mode 100644 index 0000000000..7c0ce9cbe3 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/main.svelte @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file