svelte/src/runtime/internal/Component.ts

257 lines
6.2 KiB

import { add_render_callback, flush, schedule_update, dirty_components } 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 } from './dom';
import { transition_in } from './transitions';
interface Fragment {
key: string|null;
first: null;
/* create */ c: () => void;
/* claim */ l: (nodes: any) => void;
/* hydrate */ h: () => void;
/* mount */ m: (target: HTMLElement, anchor: any) => void;
/* update */ p: (ctx: any, dirty: any) => void;
/* measure */ r: () => void;
/* fix */ f: () => void;
/* animate */ a: () => void;
/* intro */ i: (local: any) => void;
/* outro */ o: (local: any) => void;
/* destroy */ d: (detaching: 0|1) => void;
}
interface T$$ {
dirty: number[];
ctx: null|any;
bound: any;
update: () => void;
callbacks: any;
after_update: any[];
props: Record<string, 0 | string>;
fragment: null|false|Fragment;
not_equal: any;
before_update: any[];
context: Map<any, any>;
on_mount: any[];
on_destroy: any[];
skip_bound: boolean;
on_disconnect: any[];
}
export function bind(component, name, callback) {
const index = component.$$.props[name];
if (index !== undefined) {
component.$$.bound[index] = callback;
callback(component.$$.ctx[index]);
}
}
export function create_component(block) {
block && block.c();
}
export function claim_component(block, parent_nodes) {
block && block.l(parent_nodes);
}
export function mount_component(component, target, anchor, customElement) {
const { fragment, on_mount, on_destroy, after_update } = component.$$;
fragment && fragment.m(target, anchor);
if (!customElement) {
// onMount happens before the initial afterUpdate
add_render_callback(() => {
const new_on_destroy = on_mount.map(run).filter(is_function);
if (on_destroy) {
on_destroy.push(...new_on_destroy);
} else {
// Edge case - component was destroyed immediately,
// most likely as a result of a binding initialising
run_all(new_on_destroy);
}
component.$$.on_mount = [];
});
}
after_update.forEach(add_render_callback);
}
export function destroy_component(component, detaching) {
const $$ = component.$$;
if ($$.fragment !== null) {
run_all($$.on_destroy);
$$.fragment && $$.fragment.d(detaching);
// TODO null out other refs, including component.$$ (but need to
// preserve final state?)
$$.on_destroy = $$.fragment = null;
$$.ctx = [];
}
}
function make_dirty(component, i) {
if (component.$$.dirty[0] === -1) {
dirty_components.push(component);
schedule_update();
component.$$.dirty.fill(0);
}
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
}
export function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) {
const parent_component = current_component;
set_current_component(component);
const $$: T$$ = component.$$ = {
fragment: null,
ctx: null,
// state
props,
update: noop,
not_equal,
bound: blank_object(),
// lifecycle
on_mount: [],
on_destroy: [],
on_disconnect: [],
before_update: [],
after_update: [],
context: new Map(parent_component ? parent_component.$$.context : options.context || []),
// everything else
callbacks: blank_object(),
dirty,
skip_bound: false
};
let ready = false;
$$.ctx = instance
? instance(component, options.props || {}, (i, ret, ...rest) => {
const value = rest.length ? rest[0] : ret;
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
if (!$$.skip_bound && $$.bound[i]) $$.bound[i](value);
if (ready) make_dirty(component, i);
}
return ret;
})
: [];
$$.update();
ready = true;
run_all($$.before_update);
// `false` as a special case of no DOM component
$$.fragment = create_fragment ? create_fragment($$.ctx) : false;
if (options.target) {
if (options.hydrate) {
start_hydrating();
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, options.customElement);
end_hydrating();
flush();
}
set_current_component(parent_component);
}
export let SvelteElement;
if (typeof HTMLElement === 'function') {
SvelteElement = class extends HTMLElement {
$$: T$$;
$$set?: ($$props: any) => void;
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
const { on_mount } = this.$$;
this.$$.on_disconnect = on_mount.map(run).filter(is_function);
// @ts-ignore todo: improve typings
for (const key in this.$$.slotted) {
// @ts-ignore todo: improve typings
this.appendChild(this.$$.slotted[key]);
}
}
attributeChangedCallback(attr, _oldValue, newValue) {
this[attr] = newValue;
}
disconnectedCallback() {
run_all(this.$$.on_disconnect);
}
$destroy() {
destroy_component(this, 1);
this.$destroy = noop;
}
$on(type, callback) {
// TODO should this delegate to addEventListener?
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) {
if (this.$$set && !is_empty($$props)) {
this.$$.skip_bound = true;
this.$$set($$props);
this.$$.skip_bound = false;
}
}
};
}
/**
* Base class for Svelte components. Used when dev=false.
*/
export class SvelteComponent {
$$: T$$;
$$set?: ($$props: any) => void;
$destroy() {
destroy_component(this, 1);
this.$destroy = noop;
}
$on(type, callback) {
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) {
if (this.$$set && !is_empty($$props)) {
this.$$.skip_bound = true;
this.$$set($$props);
this.$$.skip_bound = false;
}
}
}