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

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

@ -1,478 +1,399 @@
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) {
component.$$.bound[index] = callback; component.$$.bound[index] = 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
add_render_callback(() => {
// onMount happens before the initial afterUpdate const new_on_destroy = component.$$.on_mount.map(run).filter(is_function);
add_render_callback(() => { // if the component was destroyed immediately
const new_on_destroy = component.$$.on_mount.map(run).filter(is_function); // it will update the `$$.on_destroy` reference to `null`.
// if the component was destroyed immediately // the destructured on_destroy may still reference to the old array
// it will update the `$$.on_destroy` reference to `null`. if (component.$$.on_destroy) {
// the destructured on_destroy may still reference to the old array component.$$.on_destroy.push(...new_on_destroy);
if (component.$$.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);
// TODO null out other refs, including component.$$ (but need to
$$.fragment && $$.fragment.d(detaching); // preserve final state?)
$$.on_destroy = $$.fragment = null;
// TODO null out other refs, including component.$$ (but need to $$.ctx = [];
// preserve final state?) }
$$.on_destroy = $$.fragment = null;
$$.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);
schedule_update(); schedule_update();
component.$$.dirty.fill(0); component.$$.dirty.fill(0);
} }
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, const parent_component = current_component;
options, set_current_component(component);
instance, /** @type {T$$} */
create_fragment, const $$ = (component.$$ = {
not_equal, fragment: null,
props, ctx: [],
append_styles, // state
dirty = [-1] props,
) { update: noop,
const parent_component = current_component; not_equal,
set_current_component(component); bound: blank_object(),
// lifecycle
const $$: T$$ = (component.$$ = { on_mount: [],
fragment: null, on_destroy: [],
ctx: [], on_disconnect: [],
before_update: [],
// state after_update: [],
props, context: new Map(options.context || (parent_component ? parent_component.$$.context : [])),
update: noop, // everything else
not_equal, callbacks: blank_object(),
bound: blank_object(), dirty,
skip_bound: false,
// lifecycle root: options.target || parent_component.$$.root
on_mount: [], });
on_destroy: [], append_styles && append_styles($$.root);
on_disconnect: [], let ready = false;
before_update: [], $$.ctx = instance
after_update: [], ? instance(component, options.props || {}, (i, ret, ...rest) => {
context: new Map(options.context || (parent_component ? parent_component.$$.context : [])), const value = rest.length ? rest[0] : ret;
if ($$.ctx && not_equal($$.ctx[i], ($$.ctx[i] = value))) {
// everything else if (!$$.skip_bound && $$.bound[i])
callbacks: blank_object(), $$.bound[i](value);
dirty, if (ready)
skip_bound: false, make_dirty(component, i);
root: options.target || parent_component.$$.root }
}); return ret;
})
append_styles && append_styles($$.root); : [];
$$.update();
let ready = false; ready = true;
run_all($$.before_update);
$$.ctx = instance // `false` as a special case of no DOM component
? instance(component, options.props || {}, (i, ret, ...rest) => { $$.fragment = create_fragment ? create_fragment($$.ctx) : false;
const value = rest.length ? rest[0] : ret; if (options.target) {
if ($$.ctx && not_equal($$.ctx[i], ($$.ctx[i] = value))) { if (options.hydrate) {
if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value); start_hydrating();
if (ready) make_dirty(component, i); const nodes = children(options.target);
} // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return ret; $$.fragment && $$.fragment.l(nodes);
}) nodes.forEach(detach);
: []; }
else {
$$.update(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
ready = true; $$.fragment && $$.fragment.c();
run_all($$.before_update); }
if (options.intro)
// `false` as a special case of no DOM component transition_in(component.$$.fragment);
$$.fragment = create_fragment ? create_fragment($$.ctx) : false; mount_component(component, options.target, options.anchor);
end_hydrating();
if (options.target) { flush();
if (options.hydrate) { }
start_hydrating(); set_current_component(parent_component);
const nodes = children(options.target);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment && $$.fragment!.l(nodes);
nodes.forEach(detach);
} else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment && $$.fragment!.c();
}
if (options.intro) transition_in(component.$$.fragment);
mount_component(component, options.target, options.anchor);
end_hydrating();
flush();
}
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[], super();
use_shadow_dom: boolean this.$$componentCtor = $$componentCtor;
) { this.$$slots = $$slots;
super(); if (use_shadow_dom) {
if (use_shadow_dom) { this.attachShadow({ mode: 'open' });
this.attachShadow({ mode: 'open' }); }
} }
} addEventListener(type, listener, options) {
// We can't determine upfront if the event is a custom event or not, so we have to
addEventListener(type: string, listener: any, options?: any): void { // listen to both. If someone uses a custom event with the same name as a regular
// We can't determine upfront if the event is a custom event or not, so we have to // browser event, this fires twice - we can't avoid that.
// listen to both. If someone uses a custom event with the same name as a regular this.$$listeners[type] = this.$$listeners[type] || [];
// browser event, this fires twice - we can't avoid that. this.$$listeners[type].push(listener);
this.$$listeners[type] = this.$$listeners[type] || []; if (this.$$component) {
this.$$listeners[type].push(listener); const unsub = this.$$component.$on(type, listener);
if (this.$$component) { this.$$listener_unsubscribe_fns.set(listener, unsub);
const unsub = this.$$component!.$on(type, listener); }
this.$$listener_unsubscribe_fns.set(listener, unsub); super.addEventListener(type, listener, options);
} }
super.addEventListener(type, listener, options); removeEventListener(type, listener, options) {
} super.removeEventListener(type, listener, options);
if (this.$$component) {
removeEventListener(type: string, listener: any, options?: any): void { const unsub = this.$$listener_unsubscribe_fns.get(listener);
super.removeEventListener(type, listener, options); if (unsub) {
if (this.$$component) { unsub();
const unsub = this.$$listener_unsubscribe_fns.get(listener); this.$$listener_unsubscribe_fns.delete(listener);
if (unsub) { }
unsub(); }
this.$$listener_unsubscribe_fns.delete(listener); }
} async connectedCallback() {
} this.$$connected = true;
} if (!this.$$component) {
// We wait one tick to let possible child slot elements be created/mounted
async connectedCallback() { await Promise.resolve();
this.$$connected = true; if (!this.$$connected) {
if (!this.$$component) { return;
// We wait one tick to let possible child slot elements be created/mounted }
await Promise.resolve(); function create_slot(name) {
return () => {
if (!this.$$connected) { let node;
return; const obj = {
} c: function create() {
node = document.createElement('slot');
function create_slot(name: string) { if (name !== 'default') {
return () => { node.setAttribute('name', name);
let node: HTMLSlotElement; }
const obj = { },
c: function create() { m: function mount(target, anchor) {
node = document.createElement('slot'); insert(target, node, anchor);
if (name !== 'default') { },
node.setAttribute('name', name); d: function destroy(detaching) {
} if (detaching) {
}, detach(node);
m: function mount(target: HTMLElement, anchor?: HTMLElement) { }
insert(target, node, anchor); }
}, };
d: function destroy(detaching: boolean) { return obj;
if (detaching) { };
detach(node); }
} const $$slots = {};
} const existing_slots = get_custom_elements_slots(this);
}; for (const name of this.$$slots) {
return obj; if (name in existing_slots) {
}; $$slots[name] = [create_slot(name)];
} }
}
const $$slots: Record<string, any> = {}; for (const attribute of this.attributes) {
const existing_slots = get_custom_elements_slots(this); // this.$$data takes precedence over this.attributes
for (const name of this.$$slots) { const name = this.$$get_prop_name(attribute.name);
if (name in existing_slots) { if (!(name in this.$$data)) {
$$slots[name] = [create_slot(name)]; this.$$data[name] = get_custom_element_value(name, attribute.value, this.$$props_definition, 'toProp');
} }
} }
this.$$component = new this.$$componentCtor({
for (const attribute of this.attributes) { target: this.shadowRoot || this,
// this.$$data takes precedence over this.attributes props: {
const name = this.$$get_prop_name(attribute.name); ...this.$$data,
if (!(name in this.$$data)) { $$slots,
this.$$data[name] = get_custom_element_value( $$scope: {
name, ctx: []
attribute.value, }
this.$$props_definition, }
'toProp' });
); for (const type in this.$$listeners) {
} for (const listener of this.$$listeners[type]) {
} const unsub = this.$$component.$on(type, listener);
this.$$listener_unsubscribe_fns.set(listener, unsub);
this.$$component = new this.$$componentCtor({ }
target: this.shadowRoot || this, }
props: { this.$$listeners = {};
...this.$$data, }
$$slots, }
$$scope: { // We don't need this when working within Svelte code, but for compatibility of people using this outside of Svelte
ctx: [] // and setting attributes through setAttribute etc, this is helpful
} attributeChangedCallback(attr, _oldValue, newValue) {
} if (this.$$reflecting)
}); return;
attr = this.$$get_prop_name(attr);
for (const type in this.$$listeners) { this.$$data[attr] = get_custom_element_value(attr, newValue, this.$$props_definition, 'toProp');
for (const listener of this.$$listeners[type]) { this.$$component.$set({ [attr]: this.$$data[attr] });
const unsub = this.$$component!.$on(type, listener); }
this.$$listener_unsubscribe_fns.set(listener, unsub); disconnectedCallback() {
} this.$$connected = false;
} // In a microtask, because this could be a move within the DOM
this.$$listeners = {}; Promise.resolve().then(() => {
} if (!this.$$connected) {
} this.$$component.$destroy();
this.$$component = undefined;
// 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 });
attributeChangedCallback(attr: string, _oldValue: any, newValue: any) { }
if (this.$$reflecting) return; $$get_prop_name(attribute_name) {
return (Object.keys(this.$$props_definition).find((key) => this.$$props_definition[key].attribute === attribute_name ||
attr = this.$$get_prop_name(attr); (!this.$$props_definition[key].attribute && key.toLowerCase() === attribute_name)) || attribute_name);
this.$$data[attr] = get_custom_element_value( }
attr, };
newValue,
this.$$props_definition,
'toProp'
);
this.$$component!.$set({ [attr]: this.$$data[attr] });
}
disconnectedCallback() {
this.$$connected = false;
// In a microtask, because this could be a move within the DOM
Promise.resolve().then(() => {
if (!this.$$connected) {
this.$$component!.$destroy();
this.$$component = undefined;
}
});
}
private $$get_prop_name(attribute_name: string): string {
return (
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
);
}
};
}
function get_custom_element_value(
prop: string,
value: any,
props_definition: Record<string, CustomElementPropDefinition>,
transform?: 'toAttribute' | 'toProp'
) {
const type = props_definition[prop]?.type;
value = type === 'Boolean' && typeof value !== 'boolean' ? value != null : value;
if (!transform || !props_definition[prop]) {
return value;
} else if (transform === 'toAttribute') {
switch (type) {
case 'Object':
case 'Array':
return value == null ? null : JSON.stringify(value);
case 'Boolean':
return value ? '' : null;
case 'Number':
return value == null ? null : value;
default:
return value;
}
} else {
switch (type) {
case 'Object':
case 'Array':
return value && JSON.parse(value);
case 'Boolean':
return value; // conversion already handled above
case 'Number':
return value != null ? +value : value;
default:
return value;
}
}
} }
/** @param {string} prop
interface CustomElementPropDefinition { * @param {any} value
attribute?: string; * @param {Record<string, CustomElementPropDefinition>} props_definition
reflect?: boolean; * @param {'toAttribute' | 'toProp'} transform
type?: 'String' | 'Boolean' | 'Number' | 'Array' | 'Object'; * @returns {any}
*/
function get_custom_element_value(prop, value, props_definition, transform) {
const type = props_definition[prop]?.type;
value = type === 'Boolean' && typeof value !== 'boolean' ? value != null : value;
if (!transform || !props_definition[prop]) {
return value;
}
else if (transform === 'toAttribute') {
switch (type) {
case 'Object':
case 'Array':
return value == null ? null : JSON.stringify(value);
case 'Boolean':
return value ? '' : null;
case 'Number':
return value == null ? null : value;
default:
return value;
}
}
else {
switch (type) {
case 'Object':
case 'Array':
return value && JSON.parse(value);
case 'Boolean':
return value; // conversion already handled above
case 'Number':
return value != null ? +value : value;
default:
return value;
}
}
} }
/** /**
* @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, const Class = class extends SvelteElement {
props_definition: Record<string, CustomElementPropDefinition>, constructor() {
slots: string[], super(Component, slots, use_shadow_dom);
accessors: string[], this.$$props_definition = props_definition;
use_shadow_dom: boolean }
) { static get observedAttributes() {
const Class = class extends SvelteElement { return Object.keys(props_definition).map((key) => (props_definition[key].attribute || key).toLowerCase());
constructor() { }
super(Component, slots, use_shadow_dom); };
this.$$props_definition = props_definition; Object.keys(props_definition).forEach((prop) => {
} Object.defineProperty(Class.prototype, prop, {
get() {
static get observedAttributes() { return this.$$component && prop in this.$$component
return Object.keys(props_definition).map((key) => ? this.$$component[prop]
(props_definition[key].attribute || key).toLowerCase() : this.$$data[prop];
); },
} set(value) {
}; value = get_custom_element_value(prop, value, props_definition);
this.$$data[prop] = value;
Object.keys(props_definition).forEach((prop) => { this.$$component?.$set({ [prop]: value });
Object.defineProperty(Class.prototype, prop, { if (props_definition[prop].reflect) {
get() { this.$$reflecting = true;
return this.$$component && prop in this.$$component const attribute_value = get_custom_element_value(prop, value, props_definition, 'toAttribute');
? this.$$component[prop] if (attribute_value == null) {
: this.$$data[prop]; this.removeAttribute(prop);
}, }
else {
set(value) { this.setAttribute(props_definition[prop].attribute || prop, attribute_value);
value = get_custom_element_value(prop, value, props_definition); }
this.$$data[prop] = value; this.$$reflecting = false;
this.$$component?.$set({ [prop]: value }); }
}
if (props_definition[prop].reflect) { });
this.$$reflecting = true; });
const attribute_value = get_custom_element_value( accessors.forEach((accessor) => {
prop, Object.defineProperty(Class.prototype, accessor, {
value, get() {
props_definition, return this.$$component?.[accessor];
'toAttribute' }
); });
if (attribute_value == null) { });
this.removeAttribute(prop); Component.element = Class;
} else { return Class;
this.setAttribute(props_definition[prop].attribute || prop, attribute_value as string);
}
this.$$reflecting = false;
}
}
});
});
accessors.forEach((accessor) => {
Object.defineProperty(Class.prototype, accessor, {
get() {
return this.$$component?.[accessor];
}
});
});
Component.element = Class as any;
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_component(this, 1);
this.$destroy = noop;
}
/** @returns {any} */
$on(type, callback) {
if (!is_function(callback)) {
return noop;
}
const callbacks = this.$$.callbacks[type] || (this.$$.callbacks[type] = []);
callbacks.push(callback);
return () => {
const index = callbacks.indexOf(callback);
if (index !== -1)
callbacks.splice(index, 1);
};
}
/** @returns {void} */
$set($$props) {
if (this.$$set && !is_empty($$props)) {
this.$$.skip_bound = true;
this.$$set($$props);
this.$$.skip_bound = false;
}
}
}
$destroy() {
destroy_component(this, 1);
this.$destroy = noop;
}
$on(type, callback) {
if (!is_function(callback)) {
return noop;
}
const callbacks = this.$$.callbacks[type] || (this.$$.callbacks[type] = []);
callbacks.push(callback);
return () => {
const index = callbacks.indexOf(callback);
if (index !== -1) callbacks.splice(index, 1);
};
}
$set($$props) { /** @typedef {Object} CustomElementPropDefinition
if (this.$$set && !is_empty($$props)) { * @property {string} [attribute]
this.$$.skip_bound = true; * @property {boolean} [reflect]
this.$$set($$props); * @property {'String'|'Boolean'|'Number'|'Array'|'Object'} [type]
this.$$.skip_bound = false; */
}
}
}
Loading…
Cancel
Save