UNPKG

svelte

Version:

Cybernetically enhanced web apps

307 lines (262 loc) 8.96 kB
/** @import { ComponentContext, Effect, TemplateNode } from '#client' */ /** @import { Component, ComponentType, SvelteComponent, MountOptions } from '../../index.js' */ import { DEV } from 'esm-env'; import { clear_text_content, create_text, get_first_child, get_next_sibling, init_operations } from './dom/operations.js'; import { HYDRATION_END, HYDRATION_ERROR, HYDRATION_START } from '../../constants.js'; import { active_effect } from './runtime.js'; import { push, pop, component_context } from './context.js'; import { component_root, branch } from './reactivity/effects.js'; import { hydrate_next, hydrate_node, hydrating, set_hydrate_node, set_hydrating } from './dom/hydration.js'; import { array_from } from '../shared/utils.js'; import { all_registered_events, handle_event_propagation, root_event_handles } from './dom/elements/events.js'; import { reset_head_anchor } from './dom/blocks/svelte-head.js'; import * as w from './warnings.js'; import * as e from './errors.js'; import { assign_nodes } from './dom/template.js'; import { is_passive_event } from '../../utils.js'; /** * This is normally true — block effects should run their intro transitions — * but is false during hydration (unless `options.intro` is `true`) and * when creating the children of a `<svelte:element>` that just changed tag */ export let should_intro = true; /** @param {boolean} value */ export function set_should_intro(value) { should_intro = value; } /** * @param {Element} text * @param {string} value * @returns {void} */ export function set_text(text, value) { // For objects, we apply string coercion (which might make things like $state array references in the template reactive) before diffing var str = value == null ? '' : typeof value === 'object' ? value + '' : value; // @ts-expect-error if (str !== (text.__t ??= text.nodeValue)) { // @ts-expect-error text.__t = str; text.nodeValue = str + ''; } } /** * Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component. * Transitions will play during the initial render unless the `intro` option is set to `false`. * * @template {Record<string, any>} Props * @template {Record<string, any>} Exports * @param {ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>} component * @param {MountOptions<Props>} options * @returns {Exports} */ export function mount(component, options) { return _mount(component, options); } /** * Hydrates a component on the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component * * @template {Record<string, any>} Props * @template {Record<string, any>} Exports * @param {ComponentType<SvelteComponent<Props>> | Component<Props, Exports, any>} component * @param {{} extends Props ? { * target: Document | Element | ShadowRoot; * props?: Props; * events?: Record<string, (e: any) => any>; * context?: Map<any, any>; * intro?: boolean; * recover?: boolean; * } : { * target: Document | Element | ShadowRoot; * props: Props; * events?: Record<string, (e: any) => any>; * context?: Map<any, any>; * intro?: boolean; * recover?: boolean; * }} options * @returns {Exports} */ export function hydrate(component, options) { init_operations(); options.intro = options.intro ?? false; const target = options.target; const was_hydrating = hydrating; const previous_hydrate_node = hydrate_node; try { var anchor = /** @type {TemplateNode} */ (get_first_child(target)); while ( anchor && (anchor.nodeType !== 8 || /** @type {Comment} */ (anchor).data !== HYDRATION_START) ) { anchor = /** @type {TemplateNode} */ (get_next_sibling(anchor)); } if (!anchor) { throw HYDRATION_ERROR; } set_hydrating(true); set_hydrate_node(/** @type {Comment} */ (anchor)); hydrate_next(); const instance = _mount(component, { ...options, anchor }); if ( hydrate_node === null || hydrate_node.nodeType !== 8 || /** @type {Comment} */ (hydrate_node).data !== HYDRATION_END ) { w.hydration_mismatch(); throw HYDRATION_ERROR; } set_hydrating(false); return /** @type {Exports} */ (instance); } catch (error) { if (error === HYDRATION_ERROR) { if (options.recover === false) { e.hydration_failed(); } // If an error occured above, the operations might not yet have been initialised. init_operations(); clear_text_content(target); set_hydrating(false); return mount(component, options); } throw error; } finally { set_hydrating(was_hydrating); set_hydrate_node(previous_hydrate_node); reset_head_anchor(); } } /** @type {Map<string, number>} */ const document_listeners = new Map(); /** * @template {Record<string, any>} Exports * @param {ComponentType<SvelteComponent<any>> | Component<any>} Component * @param {MountOptions} options * @returns {Exports} */ function _mount(Component, { target, anchor, props = {}, events, context, intro = true }) { init_operations(); var registered_events = new Set(); /** @param {Array<string>} events */ var event_handle = (events) => { for (var i = 0; i < events.length; i++) { var event_name = events[i]; if (registered_events.has(event_name)) continue; registered_events.add(event_name); var passive = is_passive_event(event_name); // Add the event listener to both the container and the document. // The container listener ensures we catch events from within in case // the outer content stops propagation of the event. target.addEventListener(event_name, handle_event_propagation, { passive }); var n = document_listeners.get(event_name); if (n === undefined) { // The document listener ensures we catch events that originate from elements that were // manually moved outside of the container (e.g. via manual portals). document.addEventListener(event_name, handle_event_propagation, { passive }); document_listeners.set(event_name, 1); } else { document_listeners.set(event_name, n + 1); } } }; event_handle(array_from(all_registered_events)); root_event_handles.add(event_handle); /** @type {Exports} */ // @ts-expect-error will be defined because the render effect runs synchronously var component = undefined; var unmount = component_root(() => { var anchor_node = anchor ?? target.appendChild(create_text()); branch(() => { if (context) { push({}); var ctx = /** @type {ComponentContext} */ (component_context); ctx.c = context; } if (events) { // We can't spread the object or else we'd lose the state proxy stuff, if it is one /** @type {any} */ (props).$$events = events; } if (hydrating) { assign_nodes(/** @type {TemplateNode} */ (anchor_node), null); } should_intro = intro; // @ts-expect-error the public typings are not what the actual function looks like component = Component(anchor_node, props) || {}; should_intro = true; if (hydrating) { /** @type {Effect} */ (active_effect).nodes_end = hydrate_node; } if (context) { pop(); } }); return () => { for (var event_name of registered_events) { target.removeEventListener(event_name, handle_event_propagation); var n = /** @type {number} */ (document_listeners.get(event_name)); if (--n === 0) { document.removeEventListener(event_name, handle_event_propagation); document_listeners.delete(event_name); } else { document_listeners.set(event_name, n); } } root_event_handles.delete(event_handle); if (anchor_node !== anchor) { anchor_node.parentNode?.removeChild(anchor_node); } }; }); mounted_components.set(component, unmount); return component; } /** * References of the components that were mounted or hydrated. * Uses a `WeakMap` to avoid memory leaks. */ let mounted_components = new WeakMap(); /** * Unmounts a component that was previously mounted using `mount` or `hydrate`. * * Since 5.13.0, if `options.outro` is `true`, [transitions](https://svelte.dev/docs/svelte/transition) will play before the component is removed from the DOM. * * Returns a `Promise` that resolves after transitions have completed if `options.outro` is true, or immediately otherwise (prior to 5.13.0, returns `void`). * * ```js * import { mount, unmount } from 'svelte'; * import App from './App.svelte'; * * const app = mount(App, { target: document.body }); * * // later... * unmount(app, { outro: true }); * ``` * @param {Record<string, any>} component * @param {{ outro?: boolean }} [options] * @returns {Promise<void>} */ export function unmount(component, options) { const fn = mounted_components.get(component); if (fn) { mounted_components.delete(component); return fn(options); } if (DEV) { w.lifecycle_double_unmount(); } return Promise.resolve(); }