handle boolean attributes

pull/8457/head
Simon Holthausen 3 years ago
parent 52f911cae4
commit 1131584add

@ -544,6 +544,9 @@ export default class Component {
extract_names(declarator.id).forEach(name => { extract_names(declarator.id).forEach(name => {
const variable = this.var_lookup.get(name); const variable = this.var_lookup.get(name);
variable.export_name = 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)) { 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)); this.warn(declarator as any, compiler_warnings.unused_export_let(this.name.name, name));
} }

@ -576,14 +576,14 @@ export default function dom(
} }
`[0] as ClassDeclaration; `[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 slots_str = [...component.slots.keys()].map(key => `"${key}"`).join(',');
const accessors_str = accessors const accessors_str = accessors
.filter(accessor => !writable_props.some(prop => prop.export_name === accessor.key.name)) .filter(accessor => !writable_props.some(prop => prop.export_name === accessor.key.name))
.map(accessor => `"${accessor.key.name}"`) .map(accessor => `"${accessor.key.name}"`)
.join(','); .join(',');
body.push( 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}]));`
); );
} }

@ -206,7 +206,10 @@ export interface AppendTarget {
export interface Var { export interface Var {
name: string; 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; injected?: boolean;
module?: boolean; module?: boolean;
mutated?: boolean; mutated?: boolean;

@ -1,7 +1,7 @@
import { add_render_callback, flush, flush_render_callbacks, schedule_update, dirty_components, tick } from './scheduler'; import { add_render_callback, flush, flush_render_callbacks, schedule_update, dirty_components, tick } from './scheduler';
import { current_component, set_current_component } from './lifecycle'; import { current_component, set_current_component } from './lifecycle';
import { blank_object, is_empty, is_function, run, run_all, noop } from './utils'; 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 { transition_in } from './transitions';
import { T$$ } from './types'; import { T$$ } from './types';
import { ComponentType } from './dev'; import { ComponentType } from './dev';
@ -151,6 +151,7 @@ if (typeof HTMLElement === 'function') {
private $$connected = false; private $$connected = false;
private $$data = {}; private $$data = {};
private $$reflecting = false; private $$reflecting = false;
private $$boolean_props: string[] = [];
constructor( constructor(
private $$componentCtor: ComponentType, private $$componentCtor: ComponentType,
@ -171,13 +172,6 @@ if (typeof HTMLElement === 'function') {
connectedCallback() { connectedCallback() {
this.$$connected = true; this.$$connected = true;
if (!this.$$component) { 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) { function create_slot(name: string) {
return () => { return () => {
let node: HTMLSlotElement; 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. // 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. // 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({ this.$$component = new this.$$componentCtor({
target: this.shadowRoot!, target: this.shadowRoot!,
props: { props: {
...this.$$data,
$$slots, $$slots,
$$scope: { $$scope: {
ctx: [] ctx: []
} }
} }
}); });
// ensures that <foo-bar boolean-attribute /> 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) { attributeChangedCallback(attr: string, _oldValue: any, newValue: any) {
if (this.$$reflecting) return; if (this.$$reflecting) return;
set_custom_element_data(this.$$data, attr, newValue); this.$$data[attr] = get_custom_element_value(attr, newValue, this.$$boolean_props);
this.$$component![attr] = this.$$data; this.$$component![attr] = this.$$data[attr];
} }
disconnectedCallback() { disconnectedCallback() {
@ -261,6 +258,10 @@ function camelToHyphen(str: string) {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); 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. * Turn a Svelte component into a custom element.
* @param Component A Svelte component constructor * @param Component A Svelte component constructor
@ -272,14 +273,18 @@ function camelToHyphen(str: string) {
*/ */
export function create_custom_element( export function create_custom_element(
Component: ComponentType, Component: ComponentType,
props: string[], props: (string | { name: string; type: 'boolean' })[],
slots: string[], slots: string[],
accessors: string[], accessors: string[],
styles?: 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 { const Class = class extends SvelteElement {
constructor() { constructor() {
super(Component, slots); super(Component, slots);
this.$$boolean_props = boolean_props;
if (styles) { if (styles) {
const style = document.createElement('style'); const style = document.createElement('style');
style.textContent = styles; style.textContent = styles;
@ -288,7 +293,7 @@ export function create_custom_element(
} }
static get observedAttributes() { static get observedAttributes() {
return props; return prop_names;
} }
}; };
@ -301,10 +306,13 @@ export function create_custom_element(
}, },
set(value) { set(value) {
this.$$data[prop] = value; this.$$data[prop] = get_custom_element_value(prop, value, boolean_props);
if (this.$$component) { if (this.$$component) {
if(should_reflect.indexOf(typeof value) !== -1) { this.$$component[prop] = value;
}
if(should_reflect.indexOf(typeof value) !== -1 || value == null) {
this.$$reflecting = true; this.$$reflecting = true;
if (value === false || value == null) { if (value === false || value == null) {
this.removeAttribute(prop); this.removeAttribute(prop);
@ -313,14 +321,11 @@ export function create_custom_element(
} }
this.$$reflecting = false; this.$$reflecting = false;
} }
this.$$component[prop] = value;
}
} }
}) })
} }
props.forEach((prop) => { prop_names.forEach((prop) => {
createProperty(prop, prop); createProperty(prop, prop);
// <c-e camelCase="foo" /> will be ce.camcelcase = "foo" // <c-e camelCase="foo" /> will be ce.camcelcase = "foo"
const lower = prop.toLowerCase(); const lower = prop.toLowerCase();

@ -1,14 +1,17 @@
import { tick } from 'svelte';
import * as assert from 'assert'; import * as assert from 'assert';
import './main.svelte'; import './main.svelte';
export default function (target) { export default async function (target) {
target.innerHTML = '<custom-element name="foo"></custom-element>'; target.innerHTML = '<custom-element name="foo"></custom-element>';
const el = target.querySelector('custom-element'); 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'; el.name = 'bar';
assert.deepEqual(el.events, ['foo', 'bar']); assert.deepEqual(events, ['foo', 'bar']);
target.innerHTML = ''; target.innerHTML = '';
assert.deepEqual(el.events, ['foo', 'bar', 'destroy']); await tick();
assert.deepEqual(events, ['foo', 'bar', 'destroy']);
} }

@ -2,7 +2,7 @@
<script> <script>
import "./my-widget.svelte"; import "./my-widget.svelte";
export let red; export let red = false;
red; red;
</script> </script>

@ -1,7 +1,7 @@
<svelte:options tag="my-widget" /> <svelte:options tag="my-widget" />
<script> <script>
export let red; export let red = false;
red; red;
</script> </script>

Loading…
Cancel
Save