From bce9c3220d09d0384b213378f6821f71e4f91715 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Mon, 16 Dec 2024 13:12:42 +0100 Subject: [PATCH] portalkey or dom element --- .../client/visitors/SveltePortal.js | 9 ++++--- packages/svelte/src/index-client.js | 2 ++ packages/svelte/src/index-server.js | 2 ++ .../client/dom/blocks/svelte-portal.js | 26 ++++++++++++------- packages/svelte/src/internal/server/index.js | 9 ++++++- .../src/internal/shared/svelte-portal.js | 15 +++++++++++ 6 files changed, 50 insertions(+), 13 deletions(-) create mode 100644 packages/svelte/src/internal/shared/svelte-portal.js diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SveltePortal.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SveltePortal.js index c232608781..cbb074bd5b 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/SveltePortal.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/SveltePortal.js @@ -17,13 +17,16 @@ export function SveltePortal(node, context) { context.state.template.push(''); if (target) { - // TODO handle reactive targets? Doesn't really make sense IMHO - const value = build_attribute_value(/** @type {AST.Attribute} */ (target).value, context); + const { value, has_state } = build_attribute_value( + /** @type {AST.Attribute} */ (target).value, + context + ); const body = /** @type {BlockStatement} */ ( context.visit(node.fragment, { ...context.state, transform: { ...context.state.transform } }) ); + const portal = b.call('$.portal', value, b.arrow([b.id('$$anchor')], body)); context.state.init.push( - b.stmt(b.call('$.portal', value.value, b.arrow([b.id('$$anchor')], body))) + b.stmt(has_state ? b.call('$.render_effect', b.thunk(portal)) : portal) ); } else { // TODO reactive sources? Doesn't really make sense IMHO diff --git a/packages/svelte/src/index-client.js b/packages/svelte/src/index-client.js index 587d766233..332156deb1 100644 --- a/packages/svelte/src/index-client.js +++ b/packages/svelte/src/index-client.js @@ -191,3 +191,5 @@ export { } from './internal/client/runtime.js'; export { createRawSnippet } from './internal/client/dom/blocks/snippet.js'; + +export { createPortalKey } from './internal/shared/svelte-portal.js'; diff --git a/packages/svelte/src/index-server.js b/packages/svelte/src/index-server.js index 0f1aff8f5a..c91d214344 100644 --- a/packages/svelte/src/index-server.js +++ b/packages/svelte/src/index-server.js @@ -38,3 +38,5 @@ export async function tick() {} export { getAllContexts, getContext, hasContext, setContext } from './internal/server/context.js'; export { createRawSnippet } from './internal/server/blocks/snippet.js'; + +export { createPortalKey } from './internal/shared/svelte-portal.js'; diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-portal.js b/packages/svelte/src/internal/client/dom/blocks/svelte-portal.js index cc86c719a2..ed01c9f853 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-portal.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-portal.js @@ -1,5 +1,6 @@ /** @import { TemplateNode } from '#client' */ import { HYDRATION_END, HYDRATION_START, HYDRATION_START_ELSE } from '../../../../constants.js'; +import { PortalKey } from '../../../shared/svelte-portal.js'; import { block, remove_nodes, render_effect } from '../../reactivity/effects.js'; import { hydrate_node, hydrating, set_hydrate_node, set_hydrating } from '../hydration.js'; import { get_next_sibling } from '../operations.js'; @@ -44,23 +45,30 @@ export function portal_outlet(node, id) { * @returns {void} */ export function portal(target, content) { - const is_css_selector = typeof target === 'string'; + if (target == null) return; + + const is_dom_node = target instanceof Element; + if (!is_dom_node && !(target instanceof PortalKey)) { + throw new Error( + 'TODO error code: target can only be a key instantiated with createPortalKey, or a DOM node' + ); + } + const portal = portals.get(target); + if (!is_dom_node && !portal) + throw new Error( + 'TODO error code: No portal found for given target. Make sure portal target exists before referencing it' + ); let previous_hydrating = false; let previous_hydrate_node = null; // TODO right now the portal is rendered before the given anchor. Is that confusing for people and they'd rather have it as // the first child _inside_ the anchor if the anchor is an element? - var anchor = is_css_selector ? document.querySelector(target) : portal?.anchor; - - if (!anchor) - throw new Error( - 'TODO error code: No portal found for given target. Make sure portal target exists before referencing it' - ); + var anchor = is_dom_node ? target : portal.anchor; if (hydrating) { previous_hydrating = true; - if (is_css_selector) { + if (is_dom_node) { // These are not SSR'd, so temporarily disable hydration to properly insert them set_hydrating(false); } else { @@ -81,7 +89,7 @@ export function portal(target, content) { }); if (previous_hydrating) { - if (is_css_selector) { + if (is_dom_node) { set_hydrating(true); } else { portal.anchor = hydrate_node; // so that next head block starts from the correct node diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index 84ff7b0e56..94e8000afd 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -18,6 +18,7 @@ import { EMPTY_COMMENT, BLOCK_CLOSE, BLOCK_OPEN } from './hydration.js'; import { validate_store } from '../shared/validate.js'; import { is_boolean_attribute, is_raw_text_element, is_void } from '../../utils.js'; import { reset_elements } from './dev.js'; +import { PortalKey } from '../shared/svelte-portal.js'; // https://html.spec.whatwg.org/multipage/syntax.html#attributes-2 // https://infra.spec.whatwg.org/#noncharacter @@ -182,7 +183,13 @@ export function portal_outlet(payload, id) { export function portal(payload, target, content) { payload.out += EMPTY_COMMENT; - if (typeof target === 'string') return; // targets a CSS selector - not possible in SSR, ignore + if (!target) return; // probably targets a DOM node - not possible in SSR, ignore + + if (!(target instanceof PortalKey)) { + throw new Error( + 'TODO error code: target can only be a key instantiated with createPortalKey, or a DOM node' + ); + } const portal = payload.portals.get(target); if (!portal) diff --git a/packages/svelte/src/internal/shared/svelte-portal.js b/packages/svelte/src/internal/shared/svelte-portal.js new file mode 100644 index 0000000000..1117ff733d --- /dev/null +++ b/packages/svelte/src/internal/shared/svelte-portal.js @@ -0,0 +1,15 @@ +export class PortalKey { + /** @param {string} [name] */ + constructor(name) { + this.v = Symbol(name); + } +} + +/** + * Creates a key for use with a ``. It connects the portal source and target. + * Example: TODO write out once exact API clear. + * @param {string} [name] + */ +export function createPortalKey(name) { + return new PortalKey(name); +}