Convert src/runtime/internal/Component.ts to JavaScript

pull/8569/head
S. Elliott Johnson 3 years ago
parent d5761c85c5
commit f1f68b834d

@ -1,24 +1,9 @@
import { import { add_render_callback, flush, flush_render_callbacks, schedule_update, dirty_components } from './scheduler';
add_render_callback,
flush,
flush_render_callbacks,
schedule_update,
dirty_components
} 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 { import { children, detach, start_hydrating, end_hydrating, get_custom_elements_slots, insert } from './dom';
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'; /** @returns {void} */
import { ComponentType } from './dev';
export function bind(component, name, callback) { export function bind(component, name, callback) {
const index = component.$$.props[name]; const index = component.$$.props[name];
if (index !== undefined) { if (index !== undefined) {
@ -26,20 +11,18 @@ export function bind(component, name, callback) {
callback(component.$$.ctx[index]); callback(component.$$.ctx[index]);
} }
} }
/** @returns {void} */
export function create_component(block) { export function create_component(block) {
block && block.c(); block && block.c();
} }
/** @returns {void} */
export function claim_component(block, parent_nodes) { export function claim_component(block, parent_nodes) {
block && block.l(parent_nodes); block && block.l(parent_nodes);
} }
/** @returns {void} */
export function mount_component(component, target, anchor) { export function mount_component(component, target, anchor) {
const { fragment, after_update } = component.$$; const { fragment, after_update } = component.$$;
fragment && fragment.m(target, anchor); fragment && fragment.m(target, anchor);
// onMount happens before the initial afterUpdate // onMount happens before the initial afterUpdate
add_render_callback(() => { add_render_callback(() => {
const new_on_destroy = component.$$.on_mount.map(run).filter(is_function); const new_on_destroy = component.$$.on_mount.map(run).filter(is_function);
@ -48,33 +31,30 @@ export function mount_component(component, target, anchor) {
// the destructured on_destroy may still reference to the old array // the destructured on_destroy may still reference to the old array
if (component.$$.on_destroy) { if (component.$$.on_destroy) {
component.$$.on_destroy.push(...new_on_destroy); component.$$.on_destroy.push(...new_on_destroy);
} else { }
else {
// Edge case - component was destroyed immediately, // Edge case - component was destroyed immediately,
// most likely as a result of a binding initialising // most likely as a result of a binding initialising
run_all(new_on_destroy); run_all(new_on_destroy);
} }
component.$$.on_mount = []; component.$$.on_mount = [];
}); });
after_update.forEach(add_render_callback); after_update.forEach(add_render_callback);
} }
/** @returns {void} */
export function destroy_component(component, detaching) { export function destroy_component(component, detaching) {
const $$ = component.$$; const $$ = component.$$;
if ($$.fragment !== null) { if ($$.fragment !== null) {
flush_render_callbacks($$.after_update); flush_render_callbacks($$.after_update);
run_all($$.on_destroy); run_all($$.on_destroy);
$$.fragment && $$.fragment.d(detaching); $$.fragment && $$.fragment.d(detaching);
// TODO null out other refs, including component.$$ (but need to // TODO null out other refs, including component.$$ (but need to
// preserve final state?) // preserve final state?)
$$.on_destroy = $$.fragment = null; $$.on_destroy = $$.fragment = null;
$$.ctx = []; $$.ctx = [];
} }
} }
/** @returns {void} */
function make_dirty(component, i) { function make_dirty(component, i) {
if (component.$$.dirty[0] === -1) { if (component.$$.dirty[0] === -1) {
dirty_components.push(component); dirty_components.push(component);
@ -83,30 +63,19 @@ function make_dirty(component, i) {
} }
component.$$.dirty[(i / 31) | 0] |= 1 << i % 31; component.$$.dirty[(i / 31) | 0] |= 1 << i % 31;
} }
/** @returns {void} */
export function init( export function init(component, options, instance, create_fragment, not_equal, props, append_styles, dirty = [-1]) {
component,
options,
instance,
create_fragment,
not_equal,
props,
append_styles,
dirty = [-1]
) {
const parent_component = current_component; const parent_component = current_component;
set_current_component(component); set_current_component(component);
/** @type {T$$} */
const $$: T$$ = (component.$$ = { const $$ = (component.$$ = {
fragment: null, fragment: null,
ctx: [], ctx: [],
// state // state
props, props,
update: noop, update: noop,
not_equal, not_equal,
bound: blank_object(), bound: blank_object(),
// lifecycle // lifecycle
on_mount: [], on_mount: [],
on_destroy: [], on_destroy: [],
@ -114,93 +83,84 @@ export function init(
before_update: [], before_update: [],
after_update: [], after_update: [],
context: new Map(options.context || (parent_component ? parent_component.$$.context : [])), context: new Map(options.context || (parent_component ? parent_component.$$.context : [])),
// everything else // everything else
callbacks: blank_object(), callbacks: blank_object(),
dirty, dirty,
skip_bound: false, skip_bound: false,
root: options.target || parent_component.$$.root root: options.target || parent_component.$$.root
}); });
append_styles && append_styles($$.root); append_styles && append_styles($$.root);
let ready = false; let ready = false;
$$.ctx = instance $$.ctx = instance
? instance(component, options.props || {}, (i, ret, ...rest) => { ? instance(component, options.props || {}, (i, ret, ...rest) => {
const value = rest.length ? rest[0] : ret; const value = rest.length ? rest[0] : ret;
if ($$.ctx && not_equal($$.ctx[i], ($$.ctx[i] = value))) { if ($$.ctx && not_equal($$.ctx[i], ($$.ctx[i] = value))) {
if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value); if (!$$.skip_bound && $$.bound[i])
if (ready) make_dirty(component, i); $$.bound[i](value);
if (ready)
make_dirty(component, i);
} }
return ret; return ret;
}) })
: []; : [];
$$.update(); $$.update();
ready = true; ready = true;
run_all($$.before_update); run_all($$.before_update);
// `false` as a special case of no DOM component // `false` as a special case of no DOM component
$$.fragment = create_fragment ? create_fragment($$.ctx) : false; $$.fragment = create_fragment ? create_fragment($$.ctx) : false;
if (options.target) { if (options.target) {
if (options.hydrate) { if (options.hydrate) {
start_hydrating(); start_hydrating();
const nodes = children(options.target); const nodes = children(options.target);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment && $$.fragment!.l(nodes); $$.fragment && $$.fragment.l(nodes);
nodes.forEach(detach); nodes.forEach(detach);
} else { }
else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment && $$.fragment!.c(); $$.fragment && $$.fragment.c();
} }
if (options.intro)
if (options.intro) transition_in(component.$$.fragment); transition_in(component.$$.fragment);
mount_component(component, options.target, options.anchor); mount_component(component, options.target, options.anchor);
end_hydrating(); end_hydrating();
flush(); flush();
} }
set_current_component(parent_component); set_current_component(parent_component);
} }
export let SvelteElement; export let SvelteElement;
if (typeof HTMLElement === 'function') { if (typeof HTMLElement === 'function') {
SvelteElement = class extends HTMLElement { SvelteElement = class extends HTMLElement {
private $$component?: SvelteComponent; $$componentCtor;
private $$connected = false; $$slots;
private $$data = {}; $$component;
private $$reflecting = false; $$connected = false;
private $$props_definition: Record<string, CustomElementPropDefinition> = {}; $$data = {};
private $$listeners: Record<string, Function[]> = {}; $$reflecting = false;
private $$listener_unsubscribe_fns = new Map<Function, Function>(); $$props_definition = {};
$$listeners = {};
constructor( $$listener_unsubscribe_fns = new Map();
private $$componentCtor: ComponentType, constructor($$componentCtor, $$slots, use_shadow_dom) {
private $$slots: string[],
use_shadow_dom: boolean
) {
super(); super();
this.$$componentCtor = $$componentCtor;
this.$$slots = $$slots;
if (use_shadow_dom) { if (use_shadow_dom) {
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
} }
} }
addEventListener(type, listener, options) {
addEventListener(type: string, listener: any, options?: any): void {
// We can't determine upfront if the event is a custom event or not, so we have to // We can't determine upfront if the event is a custom event or not, so we have to
// listen to both. If someone uses a custom event with the same name as a regular // listen to both. If someone uses a custom event with the same name as a regular
// browser event, this fires twice - we can't avoid that. // browser event, this fires twice - we can't avoid that.
this.$$listeners[type] = this.$$listeners[type] || []; this.$$listeners[type] = this.$$listeners[type] || [];
this.$$listeners[type].push(listener); this.$$listeners[type].push(listener);
if (this.$$component) { if (this.$$component) {
const unsub = this.$$component!.$on(type, listener); const unsub = this.$$component.$on(type, listener);
this.$$listener_unsubscribe_fns.set(listener, unsub); this.$$listener_unsubscribe_fns.set(listener, unsub);
} }
super.addEventListener(type, listener, options); super.addEventListener(type, listener, options);
} }
removeEventListener(type, listener, options) {
removeEventListener(type: string, listener: any, options?: any): void {
super.removeEventListener(type, listener, options); super.removeEventListener(type, listener, options);
if (this.$$component) { if (this.$$component) {
const unsub = this.$$listener_unsubscribe_fns.get(listener); const unsub = this.$$listener_unsubscribe_fns.get(listener);
@ -210,20 +170,17 @@ if (typeof HTMLElement === 'function') {
} }
} }
} }
async connectedCallback() { async connectedCallback() {
this.$$connected = true; this.$$connected = true;
if (!this.$$component) { if (!this.$$component) {
// We wait one tick to let possible child slot elements be created/mounted // We wait one tick to let possible child slot elements be created/mounted
await Promise.resolve(); await Promise.resolve();
if (!this.$$connected) { if (!this.$$connected) {
return; return;
} }
function create_slot(name) {
function create_slot(name: string) {
return () => { return () => {
let node: HTMLSlotElement; let node;
const obj = { const obj = {
c: function create() { c: function create() {
node = document.createElement('slot'); node = document.createElement('slot');
@ -231,10 +188,10 @@ if (typeof HTMLElement === 'function') {
node.setAttribute('name', name); node.setAttribute('name', name);
} }
}, },
m: function mount(target: HTMLElement, anchor?: HTMLElement) { m: function mount(target, anchor) {
insert(target, node, anchor); insert(target, node, anchor);
}, },
d: function destroy(detaching: boolean) { d: function destroy(detaching) {
if (detaching) { if (detaching) {
detach(node); detach(node);
} }
@ -243,28 +200,20 @@ if (typeof HTMLElement === 'function') {
return obj; return obj;
}; };
} }
const $$slots = {};
const $$slots: Record<string, any> = {};
const existing_slots = get_custom_elements_slots(this); const existing_slots = get_custom_elements_slots(this);
for (const name of this.$$slots) { for (const name of this.$$slots) {
if (name in existing_slots) { if (name in existing_slots) {
$$slots[name] = [create_slot(name)]; $$slots[name] = [create_slot(name)];
} }
} }
for (const attribute of this.attributes) { for (const attribute of this.attributes) {
// this.$$data takes precedence over this.attributes // this.$$data takes precedence over this.attributes
const name = this.$$get_prop_name(attribute.name); const name = this.$$get_prop_name(attribute.name);
if (!(name in this.$$data)) { if (!(name in this.$$data)) {
this.$$data[name] = get_custom_element_value( this.$$data[name] = get_custom_element_value(name, attribute.value, this.$$props_definition, 'toProp');
name,
attribute.value,
this.$$props_definition,
'toProp'
);
} }
} }
this.$$component = new this.$$componentCtor({ this.$$component = new this.$$componentCtor({
target: this.shadowRoot || this, target: this.shadowRoot || this,
props: { props: {
@ -275,66 +224,53 @@ if (typeof HTMLElement === 'function') {
} }
} }
}); });
for (const type in this.$$listeners) { for (const type in this.$$listeners) {
for (const listener of this.$$listeners[type]) { for (const listener of this.$$listeners[type]) {
const unsub = this.$$component!.$on(type, listener); const unsub = this.$$component.$on(type, listener);
this.$$listener_unsubscribe_fns.set(listener, unsub); this.$$listener_unsubscribe_fns.set(listener, unsub);
} }
} }
this.$$listeners = {}; this.$$listeners = {};
} }
} }
// We don't need this when working within Svelte code, but for compatibility of people using this outside of Svelte // We don't need this when working within Svelte code, but for compatibility of people using this outside of Svelte
// and setting attributes through setAttribute etc, this is helpful // and setting attributes through setAttribute etc, this is helpful
attributeChangedCallback(attr: string, _oldValue: any, newValue: any) { attributeChangedCallback(attr, _oldValue, newValue) {
if (this.$$reflecting) return; if (this.$$reflecting)
return;
attr = this.$$get_prop_name(attr); attr = this.$$get_prop_name(attr);
this.$$data[attr] = get_custom_element_value( this.$$data[attr] = get_custom_element_value(attr, newValue, this.$$props_definition, 'toProp');
attr, this.$$component.$set({ [attr]: this.$$data[attr] });
newValue,
this.$$props_definition,
'toProp'
);
this.$$component!.$set({ [attr]: this.$$data[attr] });
} }
disconnectedCallback() { disconnectedCallback() {
this.$$connected = false; this.$$connected = false;
// In a microtask, because this could be a move within the DOM // In a microtask, because this could be a move within the DOM
Promise.resolve().then(() => { Promise.resolve().then(() => {
if (!this.$$connected) { if (!this.$$connected) {
this.$$component!.$destroy(); this.$$component.$destroy();
this.$$component = undefined; this.$$component = undefined;
} }
}); });
} }
$$get_prop_name(attribute_name) {
private $$get_prop_name(attribute_name: string): string { return (Object.keys(this.$$props_definition).find((key) => this.$$props_definition[key].attribute === attribute_name ||
return ( (!this.$$props_definition[key].attribute && key.toLowerCase() === attribute_name)) || attribute_name);
Object.keys(this.$$props_definition).find(
(key) =>
this.$$props_definition[key].attribute === attribute_name ||
(!this.$$props_definition[key].attribute && key.toLowerCase() === attribute_name)
) || attribute_name
);
} }
}; };
} }
/** @param {string} prop
function get_custom_element_value( * @param {any} value
prop: string, * @param {Record<string, CustomElementPropDefinition>} props_definition
value: any, * @param {'toAttribute' | 'toProp'} transform
props_definition: Record<string, CustomElementPropDefinition>, * @returns {any}
transform?: 'toAttribute' | 'toProp' */
) { function get_custom_element_value(prop, value, props_definition, transform) {
const type = props_definition[prop]?.type; const type = props_definition[prop]?.type;
value = type === 'Boolean' && typeof value !== 'boolean' ? value != null : value; value = type === 'Boolean' && typeof value !== 'boolean' ? value != null : value;
if (!transform || !props_definition[prop]) { if (!transform || !props_definition[prop]) {
return value; return value;
} else if (transform === 'toAttribute') { }
else if (transform === 'toAttribute') {
switch (type) { switch (type) {
case 'Object': case 'Object':
case 'Array': case 'Array':
@ -346,7 +282,8 @@ function get_custom_element_value(
default: default:
return value; return value;
} }
} else { }
else {
switch (type) { switch (type) {
case 'Object': case 'Object':
case 'Array': case 'Array':
@ -360,44 +297,27 @@ function get_custom_element_value(
} }
} }
} }
interface CustomElementPropDefinition {
attribute?: string;
reflect?: boolean;
type?: 'String' | 'Boolean' | 'Number' | 'Array' | 'Object';
}
/** /**
* @internal * @internal
* *
* Turn a Svelte component into a custom element. * Turn a Svelte component into a custom element.
* @param Component A Svelte component constructor * @param {ComponentType} Component A Svelte component constructor
* @param props_definition The props to observe * @param {Record<string, CustomElementPropDefinition>} props_definition The props to observe
* @param slots The slots to create * @param {string[]} slots The slots to create
* @param accessors Other accessors besides the ones for props the component has * @param {string[]} accessors Other accessors besides the ones for props the component has
* @param use_shadow_dom Whether to use shadow DOM * @param {boolean} use_shadow_dom Whether to use shadow DOM
* @returns A custom element class * @returns {Class<Class>} A custom element class
*/ */
export function create_custom_element( export function create_custom_element(Component, props_definition, slots, accessors, use_shadow_dom) {
Component: ComponentType,
props_definition: Record<string, CustomElementPropDefinition>,
slots: string[],
accessors: string[],
use_shadow_dom: boolean
) {
const Class = class extends SvelteElement { const Class = class extends SvelteElement {
constructor() { constructor() {
super(Component, slots, use_shadow_dom); super(Component, slots, use_shadow_dom);
this.$$props_definition = props_definition; this.$$props_definition = props_definition;
} }
static get observedAttributes() { static get observedAttributes() {
return Object.keys(props_definition).map((key) => return Object.keys(props_definition).map((key) => (props_definition[key].attribute || key).toLowerCase());
(props_definition[key].attribute || key).toLowerCase()
);
} }
}; };
Object.keys(props_definition).forEach((prop) => { Object.keys(props_definition).forEach((prop) => {
Object.defineProperty(Class.prototype, prop, { Object.defineProperty(Class.prototype, prop, {
get() { get() {
@ -405,31 +325,24 @@ export function create_custom_element(
? this.$$component[prop] ? this.$$component[prop]
: this.$$data[prop]; : this.$$data[prop];
}, },
set(value) { set(value) {
value = get_custom_element_value(prop, value, props_definition); value = get_custom_element_value(prop, value, props_definition);
this.$$data[prop] = value; this.$$data[prop] = value;
this.$$component?.$set({ [prop]: value }); this.$$component?.$set({ [prop]: value });
if (props_definition[prop].reflect) { if (props_definition[prop].reflect) {
this.$$reflecting = true; this.$$reflecting = true;
const attribute_value = get_custom_element_value( const attribute_value = get_custom_element_value(prop, value, props_definition, 'toAttribute');
prop,
value,
props_definition,
'toAttribute'
);
if (attribute_value == null) { if (attribute_value == null) {
this.removeAttribute(prop); this.removeAttribute(prop);
} else { }
this.setAttribute(props_definition[prop].attribute || prop, attribute_value as string); else {
this.setAttribute(props_definition[prop].attribute || prop, attribute_value);
} }
this.$$reflecting = false; this.$$reflecting = false;
} }
} }
}); });
}); });
accessors.forEach((accessor) => { accessors.forEach((accessor) => {
Object.defineProperty(Class.prototype, accessor, { Object.defineProperty(Class.prototype, accessor, {
get() { get() {
@ -437,37 +350,36 @@ export function create_custom_element(
} }
}); });
}); });
Component.element = Class;
Component.element = Class as any;
return Class; return Class;
} }
/** /**
* Base class for Svelte components. Used when dev=false. * Base class for Svelte components. Used when dev=false.
*/ */
export class SvelteComponent { export class SvelteComponent {
$$: T$$; /** */
$$set?: ($$props: any) => void; $$ = undefined;
/** */
$$set = undefined;
/** @returns {void} */
$destroy() { $destroy() {
destroy_component(this, 1); destroy_component(this, 1);
this.$destroy = noop; this.$destroy = noop;
} }
/** @returns {any} */
$on(type, callback) { $on(type, callback) {
if (!is_function(callback)) { if (!is_function(callback)) {
return noop; return noop;
} }
const callbacks = this.$$.callbacks[type] || (this.$$.callbacks[type] = []); const callbacks = this.$$.callbacks[type] || (this.$$.callbacks[type] = []);
callbacks.push(callback); callbacks.push(callback);
return () => { return () => {
const index = callbacks.indexOf(callback); const index = callbacks.indexOf(callback);
if (index !== -1) callbacks.splice(index, 1); if (index !== -1)
callbacks.splice(index, 1);
}; };
} }
/** @returns {void} */
$set($$props) { $set($$props) {
if (this.$$set && !is_empty($$props)) { if (this.$$set && !is_empty($$props)) {
this.$$.skip_bound = true; this.$$.skip_bound = true;
@ -476,3 +388,12 @@ export class SvelteComponent {
} }
} }
} }
/** @typedef {Object} CustomElementPropDefinition
* @property {string} [attribute]
* @property {boolean} [reflect]
* @property {'String'|'Boolean'|'Number'|'Array'|'Object'} [type]
*/
Loading…
Cancel
Save