breaking: use quotes to differentiate custom element properties from attributes

gh-12624
Rich Harris 4 months ago
parent 448f21620f
commit 8907c23263

@ -0,0 +1,5 @@
---
'svelte': patch
---
breaking: use quotes to differentiate custom element properties from attributes

@ -4,6 +4,20 @@
> `%binding%` (%location%) is binding to a non-reactive property > `%binding%` (%location%) is binding to a non-reactive property
## custom_element_invalid_attribute
> `<%name%>` has a `%prop%` property, but it is specified as an attribute. Use `%prop%={...}`, without surrounding quotes, to treat it as a property instead
> `<%name%>` (%location%) has a `%prop%` property, but is specified as an attribute. Use `%prop%={...}`, without surrounding quotes, to treat it as a property instead
When specifying attributes and properties on custom elements, values that are quoted are treated as attributes. Unquoted values are treated as properties.
## custom_element_invalid_property
> `<%name%>` does not have a `%prop%` property. Use `%prop%="..."`, with the surrounding quotes, to treat it as an attribute instead
> `<%name%>` (%location%) does not have a `%prop%` property. Use `%prop%="..."`, with the surrounding quotes, to treat it as an attribute instead
## event_handler_invalid ## event_handler_invalid
> %handler% should be a function. Did you mean to %suggestion%? > %handler% should be a function. Did you mean to %suggestion%?

@ -34,6 +34,7 @@ import {
build_update_assignment build_update_assignment
} from './shared/utils.js'; } from './shared/utils.js';
import { visit_event_attribute } from './shared/events.js'; import { visit_event_attribute } from './shared/events.js';
import { is_array } from '../../../../../internal/shared/utils.js';
/** /**
* @param {RegularElement} node * @param {RegularElement} node
@ -632,7 +633,22 @@ function build_custom_element_attribute_update_assignment(node_id, attribute, co
const name = attribute.name; // don't lowercase, as we set the element's property, which might be case sensitive const name = attribute.name; // don't lowercase, as we set the element's property, which might be case sensitive
let { has_call, value } = build_attribute_value(attribute.value, context); let { has_call, value } = build_attribute_value(attribute.value, context);
const update = b.stmt(b.call('$.set_custom_element_data', node_id, b.literal(name), value)); // if it's quoted, it's an attribute, otherwise it's a property
const is_attribute = is_array(attribute.value);
if (dev) {
if (is_attribute && !is_ignored(attribute, 'custom_element_invalid_attribute')) {
state.init.push(b.stmt(b.call('$.warn_if_property', node_id, b.literal(name))));
}
if (!is_attribute && !is_ignored(attribute, 'custom_element_invalid_property')) {
state.init.push(b.stmt(b.call('$.warn_if_attribute', node_id, b.literal(name))));
}
}
const update = is_attribute
? b.stmt(b.call('$.set_attribute', node_id, b.literal(name), value))
: b.stmt(b.assignment('=', b.member(node_id, name), value));
if (attribute.metadata.expression.has_state) { if (attribute.metadata.expression.has_state) {
if (has_call) { if (has_call) {

@ -39,6 +39,8 @@ export const NAMESPACE_MATHML = 'http://www.w3.org/1998/Math/MathML';
// we use a list of ignorable runtime warnings because not every runtime warning // we use a list of ignorable runtime warnings because not every runtime warning
// can be ignored and we want to keep the validation for svelte-ignore in place // can be ignored and we want to keep the validation for svelte-ignore in place
export const IGNORABLE_RUNTIME_WARNINGS = /** @type {const} */ ([ export const IGNORABLE_RUNTIME_WARNINGS = /** @type {const} */ ([
'custom_element_invalid_attribute',
'custom_element_invalid_property',
'state_snapshot_uncloneable', 'state_snapshot_uncloneable',
'binding_property_non_reactive', 'binding_property_non_reactive',
'hydration_attribute_changed', 'hydration_attribute_changed',

@ -126,19 +126,26 @@ export function set_xlink_attribute(dom, attribute, value) {
} }
/** /**
* @param {any} node * @param {HTMLElement} element
* @param {string} prop * @param {string} name
* @param {any} value
*/ */
export function set_custom_element_data(node, prop, value) { export function warn_if_attribute(element, name) {
if (prop in node) { if (!get_setters(element).includes(name)) {
var curr_val = node[prop]; w.custom_element_invalid_property(element.localName, name); // TODO variant with location
var next_val = typeof curr_val === 'boolean' && value === '' ? true : value; }
if (typeof curr_val !== 'object' || curr_val !== next_val) { }
node[prop] = next_val;
} /**
} else { * @param {HTMLElement} element
set_attribute(node, prop, value); * @param {string} name
*/
export function warn_if_property(element, name) {
const attributes = /** @type {string} */ (
/** @type {any} */ (element.constructor).observedAttributes
);
if (get_setters(element).includes(name) && !attributes.includes(name)) {
w.custom_element_invalid_attribute(element.localName, name); // TODO variant with location
} }
} }
@ -173,8 +180,7 @@ export function set_attributes(
next.class = next.class ? next.class + ' ' + css_hash : css_hash; next.class = next.class ? next.class + ' ' + css_hash : css_hash;
} }
var setters = setters_cache.get(element.nodeName); var setters = get_setters(element);
if (!setters) setters_cache.set(element.nodeName, (setters = get_setters(element)));
// @ts-expect-error // @ts-expect-error
var attributes = /** @type {Record<string, unknown>} **/ (element.__attributes ??= {}); var attributes = /** @type {Record<string, unknown>} **/ (element.__attributes ??= {});
@ -325,7 +331,12 @@ export function set_dynamic_element_attributes(node, prev, next, css_hash) {
} }
for (key in next) { for (key in next) {
set_custom_element_data(node, key, next[key]); // TODO do we need to separate attributes from properties here? probably
if (get_setters(node).includes(key)) {
node[key] = next[key];
} else {
set_attribute(node, key, next[key]);
}
} }
return next; return next;
@ -355,22 +366,27 @@ var setters_cache = new Map();
/** @param {Element} element */ /** @param {Element} element */
function get_setters(element) { function get_setters(element) {
/** @type {string[]} */ var setters = setters_cache.get(element.nodeName);
var setters = [];
var descriptors; if (!setters) {
var proto = get_prototype_of(element); setters = [];
setters_cache.set(element.nodeName, setters);
// Stop at Element, from there on there's only unnecessary setters we're not interested in
while (proto.constructor.name !== 'Element') { var descriptors;
descriptors = get_descriptors(proto); var proto = get_prototype_of(element);
for (var key in descriptors) { // Stop at Element, from there on there's only unnecessary setters we're not interested in
if (descriptors[key].set && !always_set_through_set_attribute.includes(key)) { while (proto.constructor.name !== 'Element') {
setters.push(key); descriptors = get_descriptors(proto);
for (var key in descriptors) {
if (descriptors[key].set && !always_set_through_set_attribute.includes(key)) {
setters.push(key);
}
} }
}
proto = get_prototype_of(proto); proto = get_prototype_of(proto);
}
} }
return setters; return setters;

@ -28,12 +28,13 @@ export {
remove_input_defaults, remove_input_defaults,
set_attribute, set_attribute,
set_attributes, set_attributes,
set_custom_element_data,
set_dynamic_element_attributes, set_dynamic_element_attributes,
set_xlink_attribute, set_xlink_attribute,
handle_lazy_img, handle_lazy_img,
set_value, set_value,
set_checked set_checked,
warn_if_attribute,
warn_if_property
} from './dom/elements/attributes.js'; } from './dom/elements/attributes.js';
export { set_class, set_svg_class, set_mathml_class, toggle_class } from './dom/elements/class.js'; export { set_class, set_svg_class, set_mathml_class, toggle_class } from './dom/elements/class.js';
export { apply, event, delegate, replay_events } from './dom/elements/events.js'; export { apply, event, delegate, replay_events } from './dom/elements/events.js';

@ -19,6 +19,36 @@ export function binding_property_non_reactive(binding, location) {
} }
} }
/**
* `<%name%>` (%location%) has a `%prop%` property, but is specified as an attribute. Use `%prop%={...}`, without surrounding quotes, to treat it as a property instead
* @param {string} name
* @param {string} prop
* @param {string | undefined | null} [location]
*/
export function custom_element_invalid_attribute(name, prop, location) {
if (DEV) {
console.warn(`%c[svelte] custom_element_invalid_attribute\n%c${location ? `\`<${name}>\` (${location}) has a \`${prop}\` property, but is specified as an attribute. Use \`${prop}={...}\`, without surrounding quotes, to treat it as a property instead` : `\`<${name}>\` has a \`${prop}\` property, but it is specified as an attribute. Use \`${prop}={...}\`, without surrounding quotes, to treat it as a property instead`}`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn("custom_element_invalid_attribute");
}
}
/**
* `<%name%>` (%location%) does not have a `%prop%` property. Use `%prop%="..."`, with the surrounding quotes, to treat it as an attribute instead
* @param {string} name
* @param {string} prop
* @param {string | undefined | null} [location]
*/
export function custom_element_invalid_property(name, prop, location) {
if (DEV) {
console.warn(`%c[svelte] custom_element_invalid_property\n%c${location ? `\`<${name}>\` (${location}) does not have a \`${prop}\` property. Use \`${prop}="..."\`, with the surrounding quotes, to treat it as an attribute instead` : `\`<${name}>\` does not have a \`${prop}\` property. Use \`${prop}="..."\`, with the surrounding quotes, to treat it as an attribute instead`}`, bold, normal);
} else {
// TODO print a link to the documentation
console.warn("custom_element_invalid_property");
}
}
/** /**
* %handler% should be a function. Did you mean to %suggestion%? * %handler% should be a function. Did you mean to %suggestion%?
* @param {string} handler * @param {string} handler

@ -14,7 +14,8 @@
<div>hi</div> <div>hi</div>
<p>hi</p> <p>hi</p>
<button on:click={() => (red = false)}>off</button> <button on:click={() => (red = false)}>off</button>
<my-widget {red} white></my-widget> <!-- prettier-ignore -->
<my-widget red="{red || undefined}" white=""></my-widget>
<style> <style>
:host([red]) div { :host([red]) div {

@ -30,4 +30,4 @@
window.customElements.define('my-custom-inheritance-element', Extended); window.customElements.define('my-custom-inheritance-element', Extended);
</script> </script>
<my-custom-inheritance-element camelCase={{ text: 'World' }} text="!" /> <my-custom-inheritance-element camelCase={{ text: 'World' }} text={'!'} />

@ -21,12 +21,12 @@ export default function Main($$anchor) {
var custom_element_1 = $.sibling(svg_1, 2); var custom_element_1 = $.sibling(svg_1, 2);
$.template_effect(() => $.set_custom_element_data(custom_element_1, "fooBar", y())); $.template_effect(() => custom_element_1.fooBar = y());
$.template_effect(() => { $.template_effect(() => {
$.set_attribute(div, "foobar", x); $.set_attribute(div, "foobar", x);
$.set_attribute(svg, "viewBox", x); $.set_attribute(svg, "viewBox", x);
$.set_custom_element_data(custom_element, "fooBar", x); custom_element.fooBar = x;
}); });
$.append($$anchor, fragment); $.append($$anchor, fragment);

@ -20,7 +20,7 @@ export default function Skip_static_subtree($$anchor, $$props) {
var cant_skip = $.sibling(main, 2); var cant_skip = $.sibling(main, 2);
var custom_elements = $.child(cant_skip); var custom_elements = $.child(cant_skip);
$.set_custom_element_data(custom_elements, "with", "attributes"); $.set_attribute(custom_elements, "with", "attributes");
$.reset(cant_skip); $.reset(cant_skip);
$.template_effect(() => $.set_text(text, $$props.title)); $.template_effect(() => $.set_text(text, $$props.title));
$.append($$anchor, fragment); $.append($$anchor, fragment);

Loading…
Cancel
Save