From aeab7b7c018262873e5d89371ef1eb7904195bb8 Mon Sep 17 00:00:00 2001 From: 7nik Date: Mon, 28 Jul 2025 17:59:15 +0300 Subject: [PATCH] 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 b1c5b54d0a..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,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 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/internal/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index d01a39dc4f..0081dad1cf 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);