Skip to content

Commit 5e8d6ed

Browse files
fix: make defaultValue work with spread (#14640)
* fix: make `defaultValue` work with spread * chore: apply suggestions from review * tweak * only stash defaults when relevant --------- Co-authored-by: Rich Harris <[email protected]>
1 parent 432db95 commit 5e8d6ed

File tree

5 files changed

+493
-5
lines changed

5 files changed

+493
-5
lines changed

.changeset/silent-tips-cover.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: make `defaultValue` work with spread

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -358,16 +358,39 @@ export function set_attributes(
358358
} else if (key === '__value' || (key === 'value' && value != null)) {
359359
// @ts-ignore
360360
element.value = element[key] = element.__value = value;
361+
} else if (key === 'selected' && is_option_element) {
362+
set_selected(/** @type {HTMLOptionElement} */ (element), value);
361363
} else {
362364
var name = key;
363365
if (!preserve_attribute_case) {
364366
name = normalize_attribute(name);
365367
}
366368

367-
if (value == null && !is_custom_element) {
369+
var is_default = name === 'defaultValue' || name === 'defaultChecked';
370+
371+
if (value == null && !is_custom_element && !is_default) {
368372
attributes[key] = null;
369-
element.removeAttribute(key);
370-
} else if (setters.includes(name) && (is_custom_element || typeof value !== 'string')) {
373+
374+
if (name === 'value' || name === 'checked') {
375+
// removing value/checked also removes defaultValue/defaultChecked — preserve
376+
let input = /** @type {HTMLInputElement} */ (element);
377+
378+
if (name === 'value') {
379+
let prev = input.defaultValue;
380+
input.removeAttribute(name);
381+
input.defaultValue = prev;
382+
} else {
383+
let prev = input.defaultChecked;
384+
input.removeAttribute(name);
385+
input.defaultChecked = prev;
386+
}
387+
} else {
388+
element.removeAttribute(key);
389+
}
390+
} else if (
391+
is_default ||
392+
(setters.includes(name) && (is_custom_element || typeof value !== 'string'))
393+
) {
371394
// @ts-ignore
372395
element[name] = value;
373396
} else if (typeof value !== 'function') {
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
import { test } from '../../test';
2+
import { flushSync } from 'svelte';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
/**
7+
* @param {NodeListOf<any>} inputs
8+
* @param {string} field
9+
* @param {any | any[]} value
10+
*/
11+
function check_inputs(inputs, field, value) {
12+
for (let i = 0; i < inputs.length; i++) {
13+
assert.equal(inputs[i][field], Array.isArray(value) ? value[i] : value, `field ${i}`);
14+
}
15+
}
16+
17+
/**
18+
* @param {any} input
19+
* @param {string} field
20+
* @param {any} value
21+
*/
22+
function set_input(input, field, value) {
23+
input[field] = value;
24+
input.dispatchEvent(
25+
new Event(typeof value === 'boolean' ? 'change' : 'input', { bubbles: true })
26+
);
27+
}
28+
29+
/**
30+
* @param {HTMLOptionElement} option
31+
*/
32+
function select_option(option) {
33+
option.selected = true;
34+
option.dispatchEvent(new Event('change', { bubbles: true }));
35+
}
36+
37+
const after_reset = [];
38+
39+
const reset = /** @type {HTMLInputElement} */ (target.querySelector('input[type=reset]'));
40+
const [test1, test2, test3, test4, test5, test6, test7, test14] =
41+
target.querySelectorAll('div');
42+
const [test8, test9, test10, test11] = target.querySelectorAll('select');
43+
const [
44+
test1_span,
45+
test2_span,
46+
test3_span,
47+
test4_span,
48+
test5_span,
49+
test6_span,
50+
test7_span,
51+
test8_span,
52+
test9_span,
53+
test10_span,
54+
test11_span
55+
] = target.querySelectorAll('span');
56+
57+
{
58+
/** @type {NodeListOf<HTMLInputElement | HTMLTextAreaElement>} */
59+
const inputs = test1.querySelectorAll('input, textarea');
60+
check_inputs(inputs, 'value', 'x');
61+
assert.htmlEqual(test1_span.innerHTML, 'x x x x');
62+
63+
for (const input of inputs) {
64+
set_input(input, 'value', 'foo');
65+
}
66+
flushSync();
67+
check_inputs(inputs, 'value', 'foo');
68+
assert.htmlEqual(test1_span.innerHTML, 'foo foo foo foo');
69+
70+
after_reset.push(() => {
71+
check_inputs(inputs, 'value', 'x');
72+
assert.htmlEqual(test1_span.innerHTML, 'x x x x');
73+
});
74+
}
75+
76+
{
77+
/** @type {NodeListOf<HTMLInputElement | HTMLTextAreaElement>} */
78+
const inputs = test2.querySelectorAll('input, textarea');
79+
check_inputs(inputs, 'value', 'x');
80+
assert.htmlEqual(test2_span.innerHTML, 'x x x x');
81+
82+
for (const input of inputs) {
83+
set_input(input, 'value', 'foo');
84+
}
85+
flushSync();
86+
check_inputs(inputs, 'value', 'foo');
87+
assert.htmlEqual(test2_span.innerHTML, 'foo foo foo foo');
88+
89+
after_reset.push(() => {
90+
check_inputs(inputs, 'value', 'x');
91+
assert.htmlEqual(test2_span.innerHTML, 'x x x x');
92+
});
93+
}
94+
95+
{
96+
/** @type {NodeListOf<HTMLInputElement | HTMLTextAreaElement>} */
97+
const inputs = test3.querySelectorAll('input, textarea');
98+
check_inputs(inputs, 'value', 'y');
99+
assert.htmlEqual(test3_span.innerHTML, 'y y y y');
100+
101+
for (const input of inputs) {
102+
set_input(input, 'value', 'foo');
103+
}
104+
flushSync();
105+
check_inputs(inputs, 'value', 'foo');
106+
assert.htmlEqual(test3_span.innerHTML, 'foo foo foo foo');
107+
108+
after_reset.push(() => {
109+
check_inputs(inputs, 'value', 'x');
110+
assert.htmlEqual(test3_span.innerHTML, 'x x x x');
111+
});
112+
}
113+
114+
{
115+
/** @type {NodeListOf<HTMLInputElement>} */
116+
const inputs = test4.querySelectorAll('input');
117+
check_inputs(inputs, 'checked', true);
118+
assert.htmlEqual(test4_span.innerHTML, 'true true');
119+
120+
for (const input of inputs) {
121+
set_input(input, 'checked', false);
122+
}
123+
flushSync();
124+
check_inputs(inputs, 'checked', false);
125+
assert.htmlEqual(test4_span.innerHTML, 'false false');
126+
127+
after_reset.push(() => {
128+
check_inputs(inputs, 'checked', true);
129+
assert.htmlEqual(test4_span.innerHTML, 'true true');
130+
});
131+
}
132+
133+
{
134+
/** @type {NodeListOf<HTMLInputElement>} */
135+
const inputs = test5.querySelectorAll('input');
136+
check_inputs(inputs, 'checked', true);
137+
assert.htmlEqual(test5_span.innerHTML, 'true true');
138+
139+
for (const input of inputs) {
140+
set_input(input, 'checked', false);
141+
}
142+
flushSync();
143+
check_inputs(inputs, 'checked', false);
144+
assert.htmlEqual(test5_span.innerHTML, 'false false');
145+
146+
after_reset.push(() => {
147+
check_inputs(inputs, 'checked', true);
148+
assert.htmlEqual(test5_span.innerHTML, 'true true');
149+
});
150+
}
151+
152+
{
153+
/** @type {NodeListOf<HTMLInputElement>} */
154+
const inputs = test6.querySelectorAll('input');
155+
check_inputs(inputs, 'checked', false);
156+
assert.htmlEqual(test6_span.innerHTML, 'false false');
157+
158+
after_reset.push(() => {
159+
check_inputs(inputs, 'checked', true);
160+
assert.htmlEqual(test6_span.innerHTML, 'true true');
161+
});
162+
}
163+
{
164+
/** @type {NodeListOf<HTMLInputElement>} */
165+
const inputs = test7.querySelectorAll('input');
166+
check_inputs(inputs, 'checked', true);
167+
assert.htmlEqual(test7_span.innerHTML, 'true');
168+
169+
after_reset.push(() => {
170+
check_inputs(inputs, 'checked', false);
171+
assert.htmlEqual(test7_span.innerHTML, 'false');
172+
});
173+
}
174+
175+
{
176+
/** @type {NodeListOf<HTMLOptionElement>} */
177+
const options = test8.querySelectorAll('option');
178+
check_inputs(options, 'selected', [false, true, false]);
179+
assert.htmlEqual(test8_span.innerHTML, 'b');
180+
181+
select_option(options[2]);
182+
flushSync();
183+
check_inputs(options, 'selected', [false, false, true]);
184+
assert.htmlEqual(test8_span.innerHTML, 'c');
185+
186+
after_reset.push(() => {
187+
check_inputs(options, 'selected', [false, true, false]);
188+
assert.htmlEqual(test8_span.innerHTML, 'b');
189+
});
190+
}
191+
192+
{
193+
/** @type {NodeListOf<HTMLOptionElement>} */
194+
const options = test9.querySelectorAll('option');
195+
check_inputs(options, 'selected', [false, true, false]);
196+
assert.htmlEqual(test9_span.innerHTML, 'b');
197+
198+
select_option(options[2]);
199+
flushSync();
200+
check_inputs(options, 'selected', [false, false, true]);
201+
assert.htmlEqual(test9_span.innerHTML, 'c');
202+
203+
after_reset.push(() => {
204+
check_inputs(options, 'selected', [false, true, false]);
205+
assert.htmlEqual(test9_span.innerHTML, 'b');
206+
});
207+
}
208+
209+
{
210+
/** @type {NodeListOf<HTMLOptionElement>} */
211+
const options = test10.querySelectorAll('option');
212+
check_inputs(options, 'selected', [false, false, true]);
213+
assert.htmlEqual(test10_span.innerHTML, 'c');
214+
215+
select_option(options[0]);
216+
flushSync();
217+
check_inputs(options, 'selected', [true, false, false]);
218+
assert.htmlEqual(test10_span.innerHTML, 'a');
219+
220+
after_reset.push(() => {
221+
check_inputs(options, 'selected', [false, true, false]);
222+
assert.htmlEqual(test10_span.innerHTML, 'b');
223+
});
224+
}
225+
226+
{
227+
/** @type {NodeListOf<HTMLOptionElement>} */
228+
const options = test11.querySelectorAll('option');
229+
check_inputs(options, 'selected', [false, false, true]);
230+
assert.htmlEqual(test11_span.innerHTML, 'c');
231+
232+
select_option(options[0]);
233+
flushSync();
234+
check_inputs(options, 'selected', [true, false, false]);
235+
assert.htmlEqual(test11_span.innerHTML, 'a');
236+
237+
after_reset.push(() => {
238+
check_inputs(options, 'selected', [false, true, false]);
239+
assert.htmlEqual(test11_span.innerHTML, 'b');
240+
});
241+
}
242+
243+
{
244+
/** @type {NodeListOf<HTMLInputElement | HTMLTextAreaElement>} */
245+
const inputs = test14.querySelectorAll('input, textarea');
246+
assert.equal(inputs[0].value, 'x');
247+
assert.equal(/** @type {HTMLInputElement} */ (inputs[1]).checked, true);
248+
// this is still missing...i have no idea how to fix this lol
249+
// assert.equal(inputs[2].value, 'x');
250+
251+
after_reset.push(() => {
252+
assert.equal(inputs[0].value, 'y');
253+
assert.equal(/** @type {HTMLInputElement} */ (inputs[1]).checked, false);
254+
assert.equal(inputs[2].value, 'y');
255+
});
256+
}
257+
258+
reset.click();
259+
await Promise.resolve();
260+
flushSync();
261+
after_reset.forEach((fn) => fn());
262+
}
263+
});

0 commit comments

Comments
 (0)