Skip to content

fix: transform input defaults from spread #16481

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-planes-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: transform input defaults from spread
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)));
}
}
}

Expand Down Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ 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,
class_directives,
style_directives,
context,
element,
element_id
element_id,
should_remove_defaults = false
) {
/** @type {ObjectExpression['properties']} */
const values = [];
Expand Down Expand Up @@ -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
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions packages/svelte/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
37 changes: 33 additions & 4 deletions packages/svelte/src/internal/client/dom/elements/attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
*/
Expand Down Expand Up @@ -268,10 +268,30 @@ export function set_custom_element_data(node, prop, value) {
* @param {Record<string | symbol, any> | undefined} prev
* @param {Record<string | symbol, any>} next New attributes - this function mutates this object
* @param {string} [css_hash]
* @param {boolean} [should_remove_defaults]
* @param {boolean} [skip_warning]
* @returns {Record<string, any>}
*/
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];
Expand Down Expand Up @@ -465,6 +485,7 @@ export function set_attributes(element, prev, next, css_hash, skip_warning = fal
* @param {Array<() => any>} sync
* @param {Array<() => Promise<any>>} async
* @param {string} [css_hash]
* @param {boolean} [should_remove_defaults]
* @param {boolean} [skip_warning]
*/
export function attribute_effect(
Expand All @@ -473,6 +494,7 @@ export function attribute_effect(
sync = [],
async = [],
css_hash,
should_remove_defaults = false,
skip_warning = false
) {
flatten(sync, async, (values) => {
Expand All @@ -488,7 +510,14 @@ export function attribute_effect(
block(() => {
var next = fn(...values.map(get));
/** @type {Record<string | symbol, any>} */
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);
Expand Down
14 changes: 13 additions & 1 deletion packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand All @@ -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));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { test } from '../../test';

export default test({
mode: ['server'],
html: `
<input value="a">
<input type="checkbox" checked>
<input value="b">
`
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script>
let text = { defaultValue: "a" };
let checkbox = { defaultChecked: true }
</script>

<input {...text} />
<input type="checkbox" {...checkbox} />
<input value="b" {...text} />
Loading