Skip to content

Commit b3f3915

Browse files
authored
fix: set strings as attributes, non-strings as properties if property exists (#13327)
* fix: set strings as attributes, non-strings as properties if property exists * simplify * remove draggable from list, no longer needed * in fact we dont need the lookup at all * lint * beef up test * correctly SSR translate attribute
1 parent 2553932 commit b3f3915

File tree

5 files changed

+79
-26
lines changed

5 files changed

+79
-26
lines changed

.changeset/perfect-ants-allow.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: set strings as attributes, non-strings as properties if property exists

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

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,6 @@ export function set_checked(element, checked) {
8181
* @param {boolean} [skip_warning]
8282
*/
8383
export function set_attribute(element, attribute, value, skip_warning) {
84-
value = value == null ? null : value + '';
85-
8684
// @ts-expect-error
8785
var attributes = (element.__attributes ??= {});
8886

@@ -95,7 +93,7 @@ export function set_attribute(element, attribute, value, skip_warning) {
9593
(attribute === 'href' && element.nodeName === 'LINK')
9694
) {
9795
if (!skip_warning) {
98-
check_src_in_dev_hydration(element, attribute, value);
96+
check_src_in_dev_hydration(element, attribute, value ?? '');
9997
}
10098

10199
// If we reset these attributes, they would result in another network request, which we want to avoid.
@@ -113,8 +111,11 @@ export function set_attribute(element, attribute, value, skip_warning) {
113111
element[LOADING_ATTR_SYMBOL] = value;
114112
}
115113

116-
if (value === null) {
114+
if (value == null) {
117115
element.removeAttribute(attribute);
116+
} else if (attribute in element && typeof value !== 'string') {
117+
// @ts-ignore
118+
element[attribute] = value;
118119
} else {
119120
element.setAttribute(attribute, value);
120121
}
@@ -287,15 +288,15 @@ export function set_attributes(
287288
name = normalize_attribute(name);
288289
}
289290

290-
if (setters.includes(name)) {
291+
if (setters.includes(name) && typeof value !== 'string') {
292+
// @ts-ignore
293+
element[name] = value;
294+
} else if (typeof value !== 'function') {
291295
if (hydrating && (name === 'src' || name === 'href' || name === 'srcset')) {
292-
if (!skip_warning) check_src_in_dev_hydration(element, name, value);
296+
if (!skip_warning) check_src_in_dev_hydration(element, name, value ?? '');
293297
} else {
294-
// @ts-ignore
295-
element[name] = value;
298+
set_attribute(element, name, value);
296299
}
297-
} else if (typeof value !== 'function') {
298-
set_attribute(element, name, value);
299300
}
300301
}
301302
}
@@ -350,16 +351,6 @@ export function set_dynamic_element_attributes(node, prev, next, css_hash) {
350351
);
351352
}
352353

353-
/**
354-
* List of attributes that should always be set through the attr method,
355-
* because updating them through the property setter doesn't work reliably.
356-
* In the example of `width`/`height`, the problem is that the setter only
357-
* accepts numeric values, but the attribute can also be set to a string like `50%`.
358-
* In case of draggable trying to set `element.draggable='false'` will actually set
359-
* draggable to `true`. If this list becomes too big, rethink this approach.
360-
*/
361-
var always_set_through_set_attribute = ['width', 'height', 'draggable'];
362-
363354
/** @type {Map<string, string[]>} */
364355
var setters_cache = new Map();
365356

@@ -375,7 +366,7 @@ function get_setters(element) {
375366
descriptors = get_descriptors(proto);
376367

377368
for (var key in descriptors) {
378-
if (descriptors[key].set && !always_set_through_set_attribute.includes(key)) {
369+
if (descriptors[key].set) {
379370
setters.push(key);
380371
}
381372
}
@@ -389,12 +380,12 @@ function get_setters(element) {
389380
/**
390381
* @param {any} element
391382
* @param {string} attribute
392-
* @param {string | null} value
383+
* @param {string} value
393384
*/
394385
function check_src_in_dev_hydration(element, attribute, value) {
395386
if (!DEV) return;
396387
if (attribute === 'srcset' && srcset_url_equal(element, value)) return;
397-
if (src_url_equal(element.getAttribute(attribute) ?? '', value ?? '')) return;
388+
if (src_url_equal(element.getAttribute(attribute) ?? '', value)) return;
398389

399390
w.hydration_attribute_changed(
400391
attribute,
@@ -420,12 +411,12 @@ function split_srcset(srcset) {
420411

421412
/**
422413
* @param {HTMLSourceElement | HTMLImageElement} element
423-
* @param {string | undefined | null} srcset
414+
* @param {string} srcset
424415
* @returns {boolean}
425416
*/
426417
function srcset_url_equal(element, srcset) {
427418
var element_urls = split_srcset(element.srcset);
428-
var urls = split_srcset(srcset ?? '');
419+
var urls = split_srcset(srcset);
429420

430421
return (
431422
urls.length === element_urls.length &&

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,19 @@ export function head(payload, fn) {
147147
head_payload.out += BLOCK_CLOSE;
148148
}
149149

150+
/**
151+
* `<div translate={false}>` should be rendered as `<div translate="no">` and _not_
152+
* `<div translate="false">`, which is equivalent to `<div translate="yes">`. There
153+
* may be other odd cases that need to be added to this list in future
154+
* @type {Record<string, Map<any, string>>}
155+
*/
156+
const replacements = {
157+
translate: new Map([
158+
[true, 'yes'],
159+
[false, 'no']
160+
])
161+
};
162+
150163
/**
151164
* @template V
152165
* @param {string} name
@@ -156,7 +169,8 @@ export function head(payload, fn) {
156169
*/
157170
export function attr(name, value, is_boolean = false) {
158171
if (value == null || (!value && is_boolean) || (value === '' && name === 'class')) return '';
159-
const assignment = is_boolean ? '' : `="${escape_html(value, true)}"`;
172+
const normalized = (name in replacements && replacements[name].get(value)) || value;
173+
const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`;
160174
return ` ${name}${assignment}`;
161175
}
162176

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
test({ assert, target, variant, hydrate }) {
5+
function check(/** @type {boolean} */ condition) {
6+
const divs = /** @type {NodeListOf<HTMLDivElement>} */ (
7+
target.querySelectorAll(`.translate-${condition} div`)
8+
);
9+
10+
divs.forEach((div, i) => {
11+
assert.equal(div.translate, condition, `${i + 1} of ${divs.length}: ${div.outerHTML}`);
12+
});
13+
}
14+
15+
check(false);
16+
check(true);
17+
18+
if (variant === 'hydrate') {
19+
hydrate();
20+
check(false);
21+
check(true);
22+
}
23+
}
24+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<div class="translate-false">
2+
<div translate={false}></div>
3+
<div translate="no"></div>
4+
<div {...{ translate: false }}></div>
5+
<div {...{ translate: 'no' }}></div>
6+
</div>
7+
8+
<div class="translate-true">
9+
<div></div>
10+
<div translate={true}></div>
11+
<div translate="yes"></div>
12+
<div {...{ translate: true }}></div>
13+
<div {...{ translate: 'yes' }}></div>
14+
15+
<div translate="false"></div>
16+
<div translate="banana"></div>
17+
<div {...{ translate: 'false' }}></div>
18+
<div {...{ translate: 'banana' }}></div>
19+
</div>

0 commit comments

Comments
 (0)