From ebd01fa8f66afbea52e6d4f5c7e03cb18cd1cc8c Mon Sep 17 00:00:00 2001 From: daimond113 Date: Tue, 22 Jul 2025 23:51:29 +0200 Subject: [PATCH 1/5] fix: transform input defaults from spread --- .../3-transform/server/visitors/shared/element.js | 3 +++ packages/svelte/src/constants.js | 1 + packages/svelte/src/internal/server/index.js | 12 +++++++++++- .../form-default-value-from-spread/_config.js | 9 +++++++++ .../form-default-value-from-spread/main.svelte | 7 +++++++ 5 files changed, 31 insertions(+), 1 deletion(-) 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/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 7207564ef983..84692fca9c59 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 69cd213940a0..63324c860fd3 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/server/index.js b/packages/svelte/src/internal/server/index.js index 62ee22d6fcda..5ccfb7ec8e23 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 ignore_defaults = (flags & ELEMENT_IS_INPUT) === 0; for (name in attrs) { // omit functions, internal svelte properties and invalid attribute names @@ -200,6 +202,14 @@ export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) { name = name.toLowerCase(); } + if (!ignore_defaults) { + if (name === 'defaultvalue') { + name = 'value'; + } else if (name === 'defaultchecked') { + name = 'checked'; + } + } + 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 000000000000..83a2575fbf64 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/_config.js @@ -0,0 +1,9 @@ +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 000000000000..5a17622bed36 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/form-default-value-from-spread/main.svelte @@ -0,0 +1,7 @@ + + + + \ No newline at end of file From 6f6a24a8dcdf33f37730aaf4f3bcbf26189e33ee Mon Sep 17 00:00:00 2001 From: daimond113 Date: Wed, 23 Jul 2025 00:17:42 +0200 Subject: [PATCH 2/5] chore: add changeset --- .changeset/quiet-planes-doubt.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/quiet-planes-doubt.md diff --git a/.changeset/quiet-planes-doubt.md b/.changeset/quiet-planes-doubt.md new file mode 100644 index 000000000000..bd895f00ef44 --- /dev/null +++ b/.changeset/quiet-planes-doubt.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix: transform input defaults from spread From 44ac3a9950e150d7b0ff9184cba9c47a187f9cb3 Mon Sep 17 00:00:00 2001 From: daimond113 Date: Thu, 24 Jul 2025 13:20:29 +0200 Subject: [PATCH 3/5] fix: prevent duplicates --- packages/svelte/src/internal/server/index.js | 2 ++ .../samples/form-default-value-from-spread/_config.js | 1 + .../samples/form-default-value-from-spread/main.svelte | 3 ++- 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 5ccfb7ec8e23..913f526d70ec 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -205,8 +205,10 @@ export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) { if (!ignore_defaults) { if (name === 'defaultvalue') { name = 'value'; + if (attrs[name]) continue; } else if (name === 'defaultchecked') { name = 'checked'; + if (attrs[name]) continue; } } 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 index 83a2575fbf64..3808ae653044 100644 --- 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 @@ -5,5 +5,6 @@ export default test({ 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 index 5a17622bed36..7c0ce9cbe33c 100644 --- 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 @@ -4,4 +4,5 @@ - \ No newline at end of file + + \ No newline at end of file From 2d17f4af36abf9de9d463feddee0b05ea13ae2be Mon Sep 17 00:00:00 2001 From: 7nik Date: Mon, 28 Jul 2025 16:35:21 +0300 Subject: [PATCH 4/5] do not remove defaults if they are in spreads --- .../3-transform/client/visitors/RegularElement.js | 11 ++++++++++- .../src/internal/client/dom/elements/attributes.js | 12 +++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) 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 4296aa959e87..b1c5b54d0a0c 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 @@ -172,7 +172,16 @@ 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))); + const spreads = has_spread + ? b.object( + attributes + .filter((attr) => attr.type === 'SpreadAttribute') + .map((attr) => b.spread(attr.expression)) + ) + : null; + context.state.init.push( + b.stmt(b.call('$.remove_input_defaults', context.state.node, spreads)) + ); } } diff --git a/packages/svelte/src/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 22e532f5e44a..d01a39dc4f4e 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -31,14 +31,20 @@ const IS_CUSTOM_ELEMENT = Symbol('is custom element'); const IS_HTML = Symbol('is html'); /** - * The value/checked attribute in the template actually corresponds to the defaultValue property, so we need - * to remove it upon hydration to avoid a bug when someone resets the form value. + * The value/checked attribute in the template actually corresponds to the defaultValue property, + * so we need to remove it upon hydration to avoid a bug when someone resets the form value, + * unless the property is presented in the spreaded objects and is handled by `set_attributes()` * @param {HTMLInputElement} input + * @param {Record} [spread] * @returns {void} */ -export function remove_input_defaults(input) { +export function remove_input_defaults(input, spread) { if (!hydrating) return; + if (spread && (input.type === 'checkbox' ? 'defaultChecked' : 'defaultValue') in spread) { + return; + } + var already_removed = false; // We try and remove the default attributes later, rather than sync during hydration. From aeab7b7c018262873e5d89371ef1eb7904195bb8 Mon Sep 17 00:00:00 2001 From: 7nik Date: Mon, 28 Jul 2025 17:59:15 +0300 Subject: [PATCH 5/5] fix spreading twice --- .../client/visitors/RegularElement.js | 27 ++++++----- .../client/visitors/shared/element.js | 5 ++- .../client/dom/elements/attributes.js | 45 ++++++++++++++----- 3 files changed, 54 insertions(+), 23 deletions(-) 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 b1c5b54d0a0c..e90606165060 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,16 +173,12 @@ export function RegularElement(node, context) { bindings.has('group') || (!bindings.has('group') && has_value_attribute)) ) { - const spreads = has_spread - ? b.object( - attributes - .filter((attr) => attr.type === 'SpreadAttribute') - .map((attr) => b.spread(attr.expression)) - ) - : null; - context.state.init.push( - b.stmt(b.call('$.remove_input_defaults', context.state.node, spreads)) - ); + 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))); + } } } @@ -211,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 9143a570255b..4b32dab82a62 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/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index d01a39dc4f4e..0081dad1cf15 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -31,20 +31,14 @@ const IS_CUSTOM_ELEMENT = Symbol('is custom element'); const IS_HTML = Symbol('is html'); /** - * The value/checked attribute in the template actually corresponds to the defaultValue property, - * so we need to remove it upon hydration to avoid a bug when someone resets the form value, - * unless the property is presented in the spreaded objects and is handled by `set_attributes()` + * The value/checked attribute in the template actually corresponds to the defaultValue property, so we need to remove + * it upon hydration to avoid a bug when someone resets the form value * @param {HTMLInputElement} input - * @param {Record} [spread] * @returns {void} */ -export function remove_input_defaults(input, spread) { +export function remove_input_defaults(input) { if (!hydrating) return; - if (spread && (input.type === 'checkbox' ? 'defaultChecked' : 'defaultValue') in spread) { - return; - } - var already_removed = false; // We try and remove the default attributes later, rather than sync during hydration. @@ -274,10 +268,30 @@ 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) { +export function set_attributes( + element, + prev, + next, + css_hash, + should_remove_defaults = false, + skip_warning = false +) { + // prettier-ignore + if ( + hydrating && + should_remove_defaults && + element.tagName === 'INPUT' && + (/** @type {HTMLInputElement} */ (element).type === 'checkbox' + ? !('defaultChecked' in next) + : !('defaultValue' in next)) + ) { + remove_input_defaults(/** @type {HTMLInputElement} */ (element)); + } + var attributes = get_attributes(element); var is_custom_element = attributes[IS_CUSTOM_ELEMENT]; @@ -471,6 +485,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( @@ -479,6 +494,7 @@ export function attribute_effect( sync = [], async = [], css_hash, + should_remove_defaults = false, skip_warning = false ) { flatten(sync, async, (values) => { @@ -494,7 +510,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);