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
pull/13336/head
Rich Harris 12 months ago committed by GitHub
parent 2553932c2c
commit b3f3915180
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: set strings as attributes, non-strings as properties if property exists

@ -81,8 +81,6 @@ export function set_checked(element, checked) {
* @param {boolean} [skip_warning] * @param {boolean} [skip_warning]
*/ */
export function set_attribute(element, attribute, value, skip_warning) { export function set_attribute(element, attribute, value, skip_warning) {
value = value == null ? null : value + '';
// @ts-expect-error // @ts-expect-error
var attributes = (element.__attributes ??= {}); var attributes = (element.__attributes ??= {});
@ -95,7 +93,7 @@ export function set_attribute(element, attribute, value, skip_warning) {
(attribute === 'href' && element.nodeName === 'LINK') (attribute === 'href' && element.nodeName === 'LINK')
) { ) {
if (!skip_warning) { if (!skip_warning) {
check_src_in_dev_hydration(element, attribute, value); check_src_in_dev_hydration(element, attribute, value ?? '');
} }
// If we reset these attributes, they would result in another network request, which we want to avoid. // 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) {
element[LOADING_ATTR_SYMBOL] = value; element[LOADING_ATTR_SYMBOL] = value;
} }
if (value === null) { if (value == null) {
element.removeAttribute(attribute); element.removeAttribute(attribute);
} else if (attribute in element && typeof value !== 'string') {
// @ts-ignore
element[attribute] = value;
} else { } else {
element.setAttribute(attribute, value); element.setAttribute(attribute, value);
} }
@ -287,15 +288,15 @@ export function set_attributes(
name = normalize_attribute(name); name = normalize_attribute(name);
} }
if (setters.includes(name)) { if (setters.includes(name) && typeof value !== 'string') {
// @ts-ignore
element[name] = value;
} else if (typeof value !== 'function') {
if (hydrating && (name === 'src' || name === 'href' || name === 'srcset')) { if (hydrating && (name === 'src' || name === 'href' || name === 'srcset')) {
if (!skip_warning) check_src_in_dev_hydration(element, name, value); if (!skip_warning) check_src_in_dev_hydration(element, name, value ?? '');
} else { } else {
// @ts-ignore set_attribute(element, name, value);
element[name] = value;
} }
} else if (typeof value !== 'function') {
set_attribute(element, name, value);
} }
} }
} }
@ -350,16 +351,6 @@ export function set_dynamic_element_attributes(node, prev, next, css_hash) {
); );
} }
/**
* List of attributes that should always be set through the attr method,
* because updating them through the property setter doesn't work reliably.
* In the example of `width`/`height`, the problem is that the setter only
* accepts numeric values, but the attribute can also be set to a string like `50%`.
* In case of draggable trying to set `element.draggable='false'` will actually set
* draggable to `true`. If this list becomes too big, rethink this approach.
*/
var always_set_through_set_attribute = ['width', 'height', 'draggable'];
/** @type {Map<string, string[]>} */ /** @type {Map<string, string[]>} */
var setters_cache = new Map(); var setters_cache = new Map();
@ -375,7 +366,7 @@ function get_setters(element) {
descriptors = get_descriptors(proto); descriptors = get_descriptors(proto);
for (var key in descriptors) { for (var key in descriptors) {
if (descriptors[key].set && !always_set_through_set_attribute.includes(key)) { if (descriptors[key].set) {
setters.push(key); setters.push(key);
} }
} }
@ -389,12 +380,12 @@ function get_setters(element) {
/** /**
* @param {any} element * @param {any} element
* @param {string} attribute * @param {string} attribute
* @param {string | null} value * @param {string} value
*/ */
function check_src_in_dev_hydration(element, attribute, value) { function check_src_in_dev_hydration(element, attribute, value) {
if (!DEV) return; if (!DEV) return;
if (attribute === 'srcset' && srcset_url_equal(element, value)) return; if (attribute === 'srcset' && srcset_url_equal(element, value)) return;
if (src_url_equal(element.getAttribute(attribute) ?? '', value ?? '')) return; if (src_url_equal(element.getAttribute(attribute) ?? '', value)) return;
w.hydration_attribute_changed( w.hydration_attribute_changed(
attribute, attribute,
@ -420,12 +411,12 @@ function split_srcset(srcset) {
/** /**
* @param {HTMLSourceElement | HTMLImageElement} element * @param {HTMLSourceElement | HTMLImageElement} element
* @param {string | undefined | null} srcset * @param {string} srcset
* @returns {boolean} * @returns {boolean}
*/ */
function srcset_url_equal(element, srcset) { function srcset_url_equal(element, srcset) {
var element_urls = split_srcset(element.srcset); var element_urls = split_srcset(element.srcset);
var urls = split_srcset(srcset ?? ''); var urls = split_srcset(srcset);
return ( return (
urls.length === element_urls.length && urls.length === element_urls.length &&

@ -147,6 +147,19 @@ export function head(payload, fn) {
head_payload.out += BLOCK_CLOSE; head_payload.out += BLOCK_CLOSE;
} }
/**
* `<div translate={false}>` should be rendered as `<div translate="no">` and _not_
* `<div translate="false">`, which is equivalent to `<div translate="yes">`. There
* may be other odd cases that need to be added to this list in future
* @type {Record<string, Map<any, string>>}
*/
const replacements = {
translate: new Map([
[true, 'yes'],
[false, 'no']
])
};
/** /**
* @template V * @template V
* @param {string} name * @param {string} name
@ -156,7 +169,8 @@ export function head(payload, fn) {
*/ */
export function attr(name, value, is_boolean = false) { export function attr(name, value, is_boolean = false) {
if (value == null || (!value && is_boolean) || (value === '' && name === 'class')) return ''; if (value == null || (!value && is_boolean) || (value === '' && name === 'class')) return '';
const assignment = is_boolean ? '' : `="${escape_html(value, true)}"`; const normalized = (name in replacements && replacements[name].get(value)) || value;
const assignment = is_boolean ? '' : `="${escape_html(normalized, true)}"`;
return ` ${name}${assignment}`; return ` ${name}${assignment}`;
} }

@ -0,0 +1,24 @@
import { test } from '../../test';
export default test({
test({ assert, target, variant, hydrate }) {
function check(/** @type {boolean} */ condition) {
const divs = /** @type {NodeListOf<HTMLDivElement>} */ (
target.querySelectorAll(`.translate-${condition} div`)
);
divs.forEach((div, i) => {
assert.equal(div.translate, condition, `${i + 1} of ${divs.length}: ${div.outerHTML}`);
});
}
check(false);
check(true);
if (variant === 'hydrate') {
hydrate();
check(false);
check(true);
}
}
});

@ -0,0 +1,19 @@
<div class="translate-false">
<div translate={false}></div>
<div translate="no"></div>
<div {...{ translate: false }}></div>
<div {...{ translate: 'no' }}></div>
</div>
<div class="translate-true">
<div></div>
<div translate={true}></div>
<div translate="yes"></div>
<div {...{ translate: true }}></div>
<div {...{ translate: 'yes' }}></div>
<div translate="false"></div>
<div translate="banana"></div>
<div {...{ translate: 'false' }}></div>
<div {...{ translate: 'banana' }}></div>
</div>
Loading…
Cancel
Save