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 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..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,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 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/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/client/dom/elements/attributes.js b/packages/svelte/src/internal/client/dom/elements/attributes.js index 22e532f5e44a..0081dad1cf15 100644 --- a/packages/svelte/src/internal/client/dom/elements/attributes.js +++ b/packages/svelte/src/internal/client/dom/elements/attributes.js @@ -31,8 +31,8 @@ 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 * @param {HTMLInputElement} input * @returns {void} */ @@ -268,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]; @@ -465,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( @@ -473,6 +494,7 @@ export function attribute_effect( sync = [], async = [], css_hash, + should_remove_defaults = false, skip_warning = false ) { flatten(sync, async, (values) => { @@ -488,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); diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 62ee22d6fcda..913f526d70ec 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,16 @@ export function spread_attributes(attrs, css_hash, classes, styles, flags = 0) { name = name.toLowerCase(); } + if (!ignore_defaults) { + if (name === 'defaultvalue') { + name = 'value'; + if (attrs[name]) continue; + } else if (name === 'defaultchecked') { + name = '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 000000000000..3808ae653044 --- /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 000000000000..7c0ce9cbe33c --- /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