Skip to content

Commit 95b87ff

Browse files
committed
chore: more default value spread work
This is a WIP, which I'm not sure if it goes anywhere. It started out to deeper understand the fix in #14640, and to fix some more theoretical loopholes, but this turns out to not fix the issue yet, and I'm not sure if it even will, so I'm punting on it for now but putting it up for others to see.
1 parent ce373ff commit 95b87ff

File tree

3 files changed

+82
-24
lines changed

3 files changed

+82
-24
lines changed

packages/svelte/src/compiler/phases/3-transform/server/visitors/shared/element.js

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export function build_element_attributes(node, context) {
4646
/** @type {Expression | null} */
4747
let content = null;
4848

49-
let has_spread = false;
49+
let needs_spread = false;
5050
// Use the index to keep the attributes order which is important for spreading
5151
let class_index = -1;
5252
let style_index = -1;
@@ -82,8 +82,28 @@ export function build_element_attributes(node, context) {
8282
) {
8383
events_to_capture.add(attribute.name);
8484
}
85-
// the defaultValue/defaultChecked properties don't exist as attributes
86-
} else if (attribute.name !== 'defaultValue' && attribute.name !== 'defaultChecked') {
85+
} else if (
86+
(node.name === 'input' || node.name === 'textarea') &&
87+
(attribute.name === 'defaultValue' || attribute.name === 'defaultChecked')
88+
) {
89+
// If there's a defaultValue but not value attribute, we turn it into a value attribute (same for checked).
90+
// If we can't, then we need to use a spread in order to be able to detect at runtime whether or not
91+
// the value/checked value is nullish, in which case defaultValue/defaultChecked should be used.
92+
const replacement_name = attribute.name === 'defaultValue' ? 'value' : 'checked';
93+
if (
94+
!node.attributes.some(
95+
(attr) =>
96+
attr.type === 'SpreadAttribute' ||
97+
((attr.type === 'BindDirective' || attr.type === 'Attribute') &&
98+
attr.name === replacement_name)
99+
)
100+
) {
101+
attributes.push({ ...attribute, name: replacement_name });
102+
} else {
103+
needs_spread = true;
104+
attributes.push(attribute);
105+
}
106+
} else {
87107
if (attribute.name === 'class') {
88108
class_index = attributes.length;
89109
} else if (attribute.name === 'style') {
@@ -173,7 +193,7 @@ export function build_element_attributes(node, context) {
173193
}
174194
} else if (attribute.type === 'SpreadAttribute') {
175195
attributes.push(attribute);
176-
has_spread = true;
196+
needs_spread = true;
177197
if (is_load_error_element(node.name)) {
178198
events_to_capture.add('onload');
179199
events_to_capture.add('onerror');
@@ -194,7 +214,7 @@ export function build_element_attributes(node, context) {
194214
}
195215
}
196216

197-
if (class_directives.length > 0 && !has_spread) {
217+
if (class_directives.length > 0 && !needs_spread) {
198218
const class_attribute = build_class_directives(
199219
class_directives,
200220
/** @type {AST.Attribute | null} */ (attributes[class_index] ?? null)
@@ -204,7 +224,7 @@ export function build_element_attributes(node, context) {
204224
}
205225
}
206226

207-
if (style_directives.length > 0 && !has_spread) {
227+
if (style_directives.length > 0 && !needs_spread) {
208228
build_style_directives(
209229
style_directives,
210230
/** @type {AST.Attribute | null} */ (attributes[style_index] ?? null),
@@ -215,7 +235,7 @@ export function build_element_attributes(node, context) {
215235
}
216236
}
217237

218-
if (has_spread) {
238+
if (needs_spread) {
219239
build_element_spread_attributes(node, attributes, style_directives, class_directives, context);
220240
} else {
221241
for (const attribute of /** @type {AST.Attribute[]} */ (attributes)) {

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

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -344,36 +344,58 @@ export function set_attributes(
344344
element.style.cssText = value + '';
345345
} else if (key === 'autofocus') {
346346
autofocus(/** @type {HTMLElement} */ (element), Boolean(value));
347-
} else if (key === '__value' || (key === 'value' && value != null)) {
348-
// @ts-ignore
349-
element.value = element[key] = element.__value = value;
350-
} else if (key === 'selected' && is_option_element) {
347+
} else if (
348+
key === '__value' ||
349+
(key === 'value' &&
350+
// For <input> element we don't want to fall through to removeAttribute below,
351+
// because that attribute corresponds to the defaultValue, not the value property
352+
(value != null || !is_custom_element))
353+
) {
354+
set_value(element, value);
355+
} else if (key === 'checked' && (value != null || !is_custom_element)) {
356+
set_checked(element, value);
357+
} else if (is_option_element && key === 'selected') {
351358
set_selected(/** @type {HTMLOptionElement} */ (element), value);
352359
} else {
353360
var name = key;
354361
if (!preserve_attribute_case) {
355362
name = normalize_attribute(name);
356363
}
357-
let is_default_value_or_checked = name === 'defaultValue' || name === 'defaultChecked';
358364

359-
if (value == null && !is_custom_element && !is_default_value_or_checked) {
365+
if (value == null && !is_custom_element && key !== 'checked') {
360366
attributes[key] = null;
361-
// if we remove the value/checked attributes this also for some reasons reset
362-
// the default value so we need to keep track of it and reassign it after the remove
363-
let default_value_reset = /**@type {HTMLInputElement}*/ (element).defaultValue;
364-
let default_checked_reset = /**@type {HTMLInputElement}*/ (element).defaultChecked;
365367
element.removeAttribute(key);
366-
if (key === 'value') {
367-
/**@type {HTMLInputElement}*/ (element).defaultValue = default_value_reset;
368-
} else if (key === 'checked') {
369-
/**@type {HTMLInputElement}*/ (element).defaultChecked = default_checked_reset;
370-
}
371368
} else if (
372-
is_default_value_or_checked ||
373-
(setters.includes(name) && (is_custom_element || typeof value !== 'string'))
369+
// is_default_value_or_checked ||
370+
setters.includes(name) &&
371+
(is_custom_element ||
372+
typeof value !== 'string' ||
373+
name === 'defaultValue' ||
374+
name === 'defaultChecked')
374375
) {
376+
// if we adjust the value/checked attributes this also for some reasons reset
377+
// the default value so we need to keep track of it and reassign it after the remove
378+
let default_value_reset;
379+
let default_checked_reset;
380+
if (hydrating) {
381+
default_value_reset = /**@type {HTMLInputElement}*/ (element).value;
382+
default_checked_reset = /**@type {HTMLInputElement}*/ (element).checked;
383+
}
384+
375385
// @ts-ignore
376386
element[name] = value;
387+
388+
if (hydrating) {
389+
if (key === 'defaultValue') {
390+
/**@type {HTMLInputElement}*/ (element).value = /** @type {string} */ (
391+
default_value_reset
392+
);
393+
} else if (key === 'defaultChecked') {
394+
/**@type {HTMLInputElement}*/ (element).checked = /** @type {boolean} */ (
395+
default_checked_reset
396+
);
397+
}
398+
}
377399
} else if (typeof value !== 'function') {
378400
if (hydrating && (name === 'src' || name === 'href' || name === 'srcset')) {
379401
if (!skip_warning) check_src_in_dev_hydration(element, name, value ?? '');

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,22 @@ export function spread_attributes(attrs, classes, styles, flags = 0) {
224224

225225
var value = attrs[name];
226226

227+
if (is_html) {
228+
if (name === 'defaultvalue') {
229+
if (attrs.value == null) {
230+
name = 'value';
231+
} else {
232+
continue;
233+
}
234+
} else if (name === 'defaultchecked') {
235+
if (attrs.checked == null) {
236+
name = 'checked';
237+
} else {
238+
continue;
239+
}
240+
}
241+
}
242+
227243
if (lowercase) {
228244
name = name.toLowerCase();
229245
}

0 commit comments

Comments
 (0)