Skip to content

Commit a151ae1

Browse files
authored
fix: handle static form values in combination with default values (#14555)
When the `value` or `checked` attribute of an input or the contents of a textarea were static, setting the `defaulValue/defaultChecked` property caused the latter to take precedence over the former. This is due to how we transform the code: If the value is static, we put it onto
1 parent c0746e0 commit a151ae1

File tree

6 files changed

+81
-4
lines changed

6 files changed

+81
-4
lines changed

.changeset/blue-fans-greet.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'svelte': patch
3+
---
4+
5+
fix: handle static form values in combination with default values

packages/svelte/src/compiler/phases/3-transform/client/visitors/RegularElement.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ export function RegularElement(node, context) {
291291

292292
const is = is_custom_element
293293
? build_custom_element_attribute_update_assignment(node_id, attribute, context)
294-
: build_element_attribute_update_assignment(node, node_id, attribute, context);
294+
: build_element_attribute_update_assignment(node, node_id, attribute, attributes, context);
295295
if (is) is_attributes_reactive = true;
296296
}
297297
}
@@ -519,10 +519,17 @@ function setup_select_synchronization(value_binding, context) {
519519
* @param {AST.RegularElement} element
520520
* @param {Identifier} node_id
521521
* @param {AST.Attribute} attribute
522+
* @param {Array<AST.Attribute | AST.SpreadAttribute>} attributes
522523
* @param {ComponentContext} context
523524
* @returns {boolean}
524525
*/
525-
function build_element_attribute_update_assignment(element, node_id, attribute, context) {
526+
function build_element_attribute_update_assignment(
527+
element,
528+
node_id,
529+
attribute,
530+
attributes,
531+
context
532+
) {
526533
const state = context.state;
527534
const name = get_attribute_name(element, attribute);
528535
const is_svg = context.state.metadata.namespace === 'svg' || element.name === 'svg';
@@ -565,6 +572,26 @@ function build_element_attribute_update_assignment(element, node_id, attribute,
565572
update = b.stmt(b.call('$.set_checked', node_id, value));
566573
} else if (name === 'selected') {
567574
update = b.stmt(b.call('$.set_selected', node_id, value));
575+
} else if (
576+
// If we would just set the defaultValue property, it would override the value property,
577+
// because it is set in the template which implicitly means it's also setting the default value,
578+
// and if one updates the default value while the input is pristine it will also update the
579+
// current value, which is not what we want, which is why we need to do some extra work.
580+
name === 'defaultValue' &&
581+
(attributes.some(
582+
(attr) => attr.type === 'Attribute' && attr.name === 'value' && is_text_attribute(attr)
583+
) ||
584+
(element.name === 'textarea' && element.fragment.nodes.length > 0))
585+
) {
586+
update = b.stmt(b.call('$.set_default_value', node_id, value));
587+
} else if (
588+
// See defaultValue comment
589+
name === 'defaultChecked' &&
590+
attributes.some(
591+
(attr) => attr.type === 'Attribute' && attr.name === 'checked' && attr.value === true
592+
)
593+
) {
594+
update = b.stmt(b.call('$.set_default_checked', node_id, value));
568595
} else if (is_dom_property(name)) {
569596
update = b.stmt(b.assignment('=', b.member(node_id, name), value));
570597
} else {

packages/svelte/src/internal/client/dom/elements/attributes.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,28 @@ export function set_selected(element, selected) {
103103
}
104104
}
105105

106+
/**
107+
* Applies the default checked property without influencing the current checked property.
108+
* @param {HTMLInputElement} element
109+
* @param {boolean} checked
110+
*/
111+
export function set_default_checked(element, checked) {
112+
const existing_value = element.checked;
113+
element.defaultChecked = checked;
114+
element.checked = existing_value;
115+
}
116+
117+
/**
118+
* Applies the default value property without influencing the current value property.
119+
* @param {HTMLInputElement | HTMLTextAreaElement} element
120+
* @param {string} value
121+
*/
122+
export function set_default_value(element, value) {
123+
const existing_value = element.value;
124+
element.defaultValue = value;
125+
element.value = existing_value;
126+
}
127+
106128
/**
107129
* @param {Element} element
108130
* @param {string} attribute

packages/svelte/src/internal/client/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ export {
3535
handle_lazy_img,
3636
set_value,
3737
set_checked,
38-
set_selected
38+
set_selected,
39+
set_default_checked,
40+
set_default_value
3941
} from './dom/elements/attributes.js';
4042
export { set_class, set_svg_class, set_mathml_class, toggle_class } from './dom/elements/class.js';
4143
export { apply, event, delegate, replay_events } from './dom/elements/events.js';

packages/svelte/tests/runtime-runes/samples/form-default-value/_config.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export default test({
3737
const after_reset = [];
3838

3939
const reset = /** @type {HTMLInputElement} */ (target.querySelector('input[type=reset]'));
40-
const [test1, test2, test3, test4, test5] = target.querySelectorAll('div');
40+
const [test1, test2, test3, test4, test5, test12] = target.querySelectorAll('div');
4141
const [test6, test7, test8, test9] = target.querySelectorAll('select');
4242
const [
4343
test1_span,
@@ -201,6 +201,20 @@ export default test({
201201
});
202202
}
203203

204+
{
205+
/** @type {NodeListOf<HTMLInputElement | HTMLTextAreaElement>} */
206+
const inputs = test12.querySelectorAll('input, textarea');
207+
assert.equal(inputs[0].value, 'x');
208+
assert.equal(/** @type {HTMLInputElement} */ (inputs[1]).checked, true);
209+
assert.equal(inputs[2].value, 'x');
210+
211+
after_reset.push(() => {
212+
assert.equal(inputs[0].value, 'y');
213+
assert.equal(/** @type {HTMLInputElement} */ (inputs[1]).checked, false);
214+
assert.equal(inputs[2].value, 'y');
215+
});
216+
}
217+
204218
reset.click();
205219
await Promise.resolve();
206220
flushSync();

packages/svelte/tests/runtime-runes/samples/form-default-value/main.svelte

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,13 @@
137137
<option value="c">C</option>
138138
</select>
139139

140+
<p>Static values</p>
141+
<div class="test-12">
142+
<input value="x" defaultValue="y" />
143+
<input type="checkbox" checked defaultChecked={false} />
144+
<textarea defaultValue="y">x</textarea>
145+
</div>
146+
140147
<input type="reset" value="Reset" />
141148
</form>
142149

0 commit comments

Comments
 (0)