diff --git a/src/compiler/compile/Component.ts b/src/compiler/compile/Component.ts index b2c2fff248..c90272620c 100644 --- a/src/compiler/compile/Component.ts +++ b/src/compiler/compile/Component.ts @@ -544,6 +544,9 @@ export default class Component { extract_names(declarator.id).forEach(name => { const variable = this.var_lookup.get(name); variable.export_name = name; + if (declarator.init?.type === 'Literal' && typeof declarator.init.value === 'boolean') { + variable.is_boolean = true; + } if (!module_script && variable.writable && !(variable.referenced || variable.referenced_from_script || variable.subscribable)) { this.warn(declarator as any, compiler_warnings.unused_export_let(this.name.name, name)); } diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index 058c132281..200dbdb2ba 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -576,14 +576,14 @@ export default function dom( } `[0] as ClassDeclaration; - const props_str = writable_props.map(prop => `"${prop.export_name}"`).join(','); + const props_str = JSON.stringify(writable_props.map(prop => prop.is_boolean ? { name: prop.export_name, type: 'boolean' } : prop.export_name)); const slots_str = [...component.slots.keys()].map(key => `"${key}"`).join(','); const accessors_str = accessors .filter(accessor => !writable_props.some(prop => prop.export_name === accessor.key.name)) .map(accessor => `"${accessor.key.name}"`) .join(','); body.push( - b`@_customElements.define("${component.tag}", @create_custom_element(${name}, [${props_str}], [${slots_str}], [${accessors_str}]));` + b`@_customElements.define("${component.tag}", @create_custom_element(${name}, ${props_str}, [${slots_str}], [${accessors_str}]));` ); } diff --git a/src/compiler/interfaces.ts b/src/compiler/interfaces.ts index 4ae53594bb..5767f1523d 100644 --- a/src/compiler/interfaces.ts +++ b/src/compiler/interfaces.ts @@ -206,7 +206,10 @@ export interface AppendTarget { export interface Var { name: string; - export_name?: string; // the `bar` in `export { foo as bar }` + /** the `bar` in `export { foo as bar }` or `export let bar` */ + export_name?: string; + /** true if assigned a boolean default value (`export let foo = true`) */ + is_boolean?: boolean; injected?: boolean; module?: boolean; mutated?: boolean; diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index 2a1655a896..b370bfbff0 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -1,7 +1,7 @@ import { add_render_callback, flush, flush_render_callbacks, schedule_update, dirty_components, tick } from './scheduler'; import { current_component, set_current_component } from './lifecycle'; import { blank_object, is_empty, is_function, run, run_all, noop } from './utils'; -import { children, detach, start_hydrating, end_hydrating, set_custom_element_data, get_custom_elements_slots, insert } from './dom'; +import { children, detach, start_hydrating, end_hydrating, get_custom_elements_slots, insert } from './dom'; import { transition_in } from './transitions'; import { T$$ } from './types'; import { ComponentType } from './dev'; @@ -151,6 +151,7 @@ if (typeof HTMLElement === 'function') { private $$connected = false; private $$data = {}; private $$reflecting = false; + private $$boolean_props: string[] = []; constructor( private $$componentCtor: ComponentType, @@ -171,13 +172,6 @@ if (typeof HTMLElement === 'function') { connectedCallback() { this.$$connected = true; if (!this.$$component) { - for (const attribute of this.attributes) { - // this.$$data takes precedence over this.attributes - if (!(attribute.name in this.$$data)) { - this.$$data[attribute.name] = attribute.value; - } - } - function create_slot(name: string) { return () => { let node: HTMLSlotElement; @@ -209,22 +203,25 @@ if (typeof HTMLElement === 'function') { } } + for (const attribute of this.attributes) { + // this.$$data takes precedence over this.attributes + if (!(attribute.name in this.$$data)) { + this.$$data[attribute.name] = get_custom_element_value(attribute.name, attribute.value, this.$$boolean_props); + } + } + // Dilemma: We need to set the component props eagerly or they have the wrong value for actions/onMount etc. // Boolean attributes are represented by the empty string, and we don't know if they represent boolean or string props. this.$$component = new this.$$componentCtor({ target: this.shadowRoot!, props: { + ...this.$$data, $$slots, $$scope: { ctx: [] } } }); - // ensures that works correctly - Object.keys(this.$$data).forEach(key => { - set_custom_element_data(this, key, this.$$data[key]); - this.$$data[key] = this[key]; // "" -> true for boolean attributes - }); } } @@ -233,8 +230,8 @@ if (typeof HTMLElement === 'function') { attributeChangedCallback(attr: string, _oldValue: any, newValue: any) { if (this.$$reflecting) return; - set_custom_element_data(this.$$data, attr, newValue); - this.$$component![attr] = this.$$data; + this.$$data[attr] = get_custom_element_value(attr, newValue, this.$$boolean_props); + this.$$component![attr] = this.$$data[attr]; } disconnectedCallback() { @@ -261,6 +258,10 @@ function camelToHyphen(str: string) { return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); } +function get_custom_element_value(prop, value, boolean_attrs) { + return value === '' && boolean_attrs.indexOf(prop) !== -1 ? true : value; +} + /** * Turn a Svelte component into a custom element. * @param Component A Svelte component constructor @@ -272,14 +273,18 @@ function camelToHyphen(str: string) { */ export function create_custom_element( Component: ComponentType, - props: string[], + props: (string | { name: string; type: 'boolean' })[], slots: string[], accessors: string[], styles?: string, ) { + const prop_names = props.map((prop) => (typeof prop === 'string' ? prop : prop.name)); + const boolean_props = props.filter((prop) => typeof prop !== 'string').map((prop) => (prop as { name:string }).name); + const Class = class extends SvelteElement { constructor() { super(Component, slots); + this.$$boolean_props = boolean_props; if (styles) { const style = document.createElement('style'); style.textContent = styles; @@ -288,7 +293,7 @@ export function create_custom_element( } static get observedAttributes() { - return props; + return prop_names; } }; @@ -301,26 +306,26 @@ export function create_custom_element( }, set(value) { - this.$$data[prop] = value; + this.$$data[prop] = get_custom_element_value(prop, value, boolean_props); if (this.$$component) { - if(should_reflect.indexOf(typeof value) !== -1) { - this.$$reflecting = true; - if (value === false || value == null) { - this.removeAttribute(prop); - } else { - this.setAttribute(prop, value); - } - this.$$reflecting = false; - } - this.$$component[prop] = value; } + + if(should_reflect.indexOf(typeof value) !== -1 || value == null) { + this.$$reflecting = true; + if (value === false || value == null) { + this.removeAttribute(prop); + } else { + this.setAttribute(prop, value); + } + this.$$reflecting = false; + } } }) } - props.forEach((prop) => { + prop_names.forEach((prop) => { createProperty(prop, prop); // will be ce.camcelcase = "foo" const lower = prop.toLowerCase(); diff --git a/test/custom-elements/samples/action/test.js b/test/custom-elements/samples/action/test.js index 6bf1d3850a..e3fa0a808f 100644 --- a/test/custom-elements/samples/action/test.js +++ b/test/custom-elements/samples/action/test.js @@ -1,14 +1,17 @@ +import { tick } from 'svelte'; import * as assert from 'assert'; import './main.svelte'; -export default function (target) { +export default async function (target) { target.innerHTML = ''; const el = target.querySelector('custom-element'); - assert.deepEqual(el.events, ['foo']); + const events = el.events; // need to get the array reference, else it's gone when destroyed + assert.deepEqual(events, ['foo']); el.name = 'bar'; - assert.deepEqual(el.events, ['foo', 'bar']); + assert.deepEqual(events, ['foo', 'bar']); target.innerHTML = ''; - assert.deepEqual(el.events, ['foo', 'bar', 'destroy']); + await tick(); + assert.deepEqual(events, ['foo', 'bar', 'destroy']); } diff --git a/test/custom-elements/samples/reflect-attributes/main.svelte b/test/custom-elements/samples/reflect-attributes/main.svelte index 4aadb8bc06..726b0838c9 100644 --- a/test/custom-elements/samples/reflect-attributes/main.svelte +++ b/test/custom-elements/samples/reflect-attributes/main.svelte @@ -2,7 +2,7 @@ diff --git a/test/custom-elements/samples/reflect-attributes/my-widget.svelte b/test/custom-elements/samples/reflect-attributes/my-widget.svelte index ef6d071d2c..02685c8cb3 100644 --- a/test/custom-elements/samples/reflect-attributes/my-widget.svelte +++ b/test/custom-elements/samples/reflect-attributes/my-widget.svelte @@ -1,7 +1,7 @@