make it `{#portal ...} / {@portal ...}`

portals
Simon Holthausen 1 month ago
parent 09696b9ebb
commit a670a89bc3
No known key found for this signature in database

@ -1121,12 +1121,12 @@ export function expected_attribute_value(node) {
}
/**
* Expected 'if', 'each', 'await', 'key' or 'snippet'
* Expected 'if', 'each', 'await', 'key', 'portal' or 'snippet'
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function expected_block_type(node) {
e(node, 'expected_block_type', `Expected 'if', 'each', 'await', 'key' or 'snippet'\nhttps://svelte.dev/e/expected_block_type`);
e(node, 'expected_block_type', `Expected 'if', 'each', 'await', 'key', 'portal' or 'snippet'\nhttps://svelte.dev/e/expected_block_type`);
}
/**
@ -1734,4 +1734,4 @@ export function unterminated_string_constant(node) {
*/
export function void_element_invalid_content(node) {
e(node, 'void_element_invalid_content', `Void elements cannot have children or closing tags\nhttps://svelte.dev/e/void_element_invalid_content`);
}
}

@ -381,6 +381,20 @@ export function convert(source, ast) {
return { ...node, type: 'EventHandler' };
},
// @ts-expect-error
PortalBlock(node, { visit }) {
remove_surrounding_whitespace_nodes(node.fragment.nodes);
return {
type: 'PortalBlock',
start: node.start,
end: node.end,
expression: node.expression,
children: node.fragment.nodes.map((child) => visit(child))
};
},
PortalTag(node) {
return node;
},
// @ts-expect-error
SnippetBlock(node, { visit }) {
remove_surrounding_whitespace_nodes(node.body.nodes);
return {

@ -57,8 +57,7 @@ const meta_tags = new Map([
['svelte:component', 'SvelteComponent'],
['svelte:self', 'SvelteSelf'],
['svelte:fragment', 'SvelteFragment'],
['svelte:boundary', 'SvelteBoundary'],
['svelte:portal', 'SveltePortal']
['svelte:boundary', 'SvelteBoundary']
]);
/** @param {Parser} parser */

@ -445,6 +445,32 @@ function open(parser) {
return;
}
if (parser.eat('portal')) {
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
/** @type {AST.PortalBlock} */
const block = parser.append({
type: 'PortalBlock',
start,
end: -1,
expression,
fragment: create_fragment(),
metadata: {
expression: new ExpressionMetadata()
}
});
parser.stack.push(block);
parser.fragments.push(block.fragment);
return;
}
if (parser.eat('snippet')) {
parser.require_whitespace();
@ -675,6 +701,9 @@ function close(parser) {
case 'KeyBlock':
matched = parser.eat('key', true, false);
break;
case 'PortalBlock':
matched = parser.eat('portal', true, false);
break;
case 'AwaitBlock':
matched = parser.eat('await', true, false);
break;
@ -847,5 +876,27 @@ function special(parser) {
});
return;
}
if (parser.eat('portal')) {
// {@portal key}
parser.require_whitespace();
const expression = read_expression(parser);
parser.allow_whitespace();
parser.eat('}', true);
parser.append({
type: 'PortalTag',
start,
end: parser.index,
expression,
metadata: {
expression: new ExpressionMetadata(),
path: []
}
});
return;
}
e.expected_tag(parser.index);
}

@ -284,7 +284,7 @@ function apply_selector(
* @param {number} from
* @param {number} to
* @param {Compiler.AST.CSS.Rule} rule
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.PortalTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {Direction} direction
* @returns {boolean}
*/
@ -330,6 +330,7 @@ function apply_combinator(relative_selector, relative_selectors, from, to, rule,
for (const possible_sibling of siblings.keys()) {
if (
possible_sibling.type === 'RenderTag' ||
possible_sibling.type === 'PortalTag' ||
possible_sibling.type === 'SlotElement' ||
possible_sibling.type === 'Component'
) {
@ -830,7 +831,7 @@ function unquote(str) {
}
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.PortalTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen
*/
@ -900,7 +901,7 @@ function get_ancestor_elements(node, adjacent_only, seen = new Set()) {
}
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.PortalTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen
*/
@ -934,7 +935,7 @@ function get_descendant_elements(node, adjacent_only, seen = new Set()) {
});
}
walk_children(node.type === 'RenderTag' ? node : node.fragment);
walk_children(node.type === 'RenderTag' || node.type === 'PortalTag' ? node : node.fragment);
// Special handling for <selectedcontent>: it clones the content of the selected <option>,
// so descendants of <option> elements in the same <select> should also be considered descendants
@ -968,7 +969,7 @@ function get_descendant_elements(node, adjacent_only, seen = new Set()) {
}
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.PortalTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @returns {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | null}
*/
function get_element_parent(node) {
@ -987,14 +988,14 @@ function get_element_parent(node) {
}
/**
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.RenderTag | Compiler.AST.PortalTag | Compiler.AST.Component | Compiler.AST.SvelteComponent | Compiler.AST.SvelteSelf} node
* @param {Direction} direction
* @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag | Compiler.AST.Component, NodeExistsValue>}
* @returns {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag | Compiler.AST.PortalTag | Compiler.AST.Component, NodeExistsValue>}
*/
function get_possible_element_siblings(node, direction, adjacent_only, seen = new Set()) {
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag | Compiler.AST.Component, NodeExistsValue>} */
/** @type {Map<Compiler.AST.RegularElement | Compiler.AST.SvelteElement | Compiler.AST.SlotElement | Compiler.AST.RenderTag | Compiler.AST.PortalTag | Compiler.AST.Component, NodeExistsValue>} */
const result = new Map();
const path = node.metadata.path;
@ -1045,6 +1046,8 @@ function get_possible_element_siblings(node, direction, adjacent_only, seen = ne
for (const snippet of node.metadata.snippets) {
add_to_map(get_possible_nested_siblings(snippet, direction, adjacent_only), result);
}
} else if (node.type === 'PortalTag') {
result.set(node, NODE_PROBABLY_EXISTS);
}
j = direction === FORWARD ? j + 1 : j - 1;
@ -1088,7 +1091,7 @@ function get_possible_element_siblings(node, direction, adjacent_only, seen = ne
}
/**
* @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement | Compiler.AST.SnippetBlock | Compiler.AST.Component} node
* @param {Compiler.AST.EachBlock | Compiler.AST.IfBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.PortalBlock | Compiler.AST.SlotElement | Compiler.AST.SnippetBlock | Compiler.AST.Component} node
* @param {Direction} direction
* @param {boolean} adjacent_only
* @param {Set<Compiler.AST.SnippetBlock>} seen
@ -1112,6 +1115,7 @@ function get_possible_nested_siblings(node, direction, adjacent_only, seen = new
break;
case 'KeyBlock':
case 'PortalBlock':
case 'SlotElement':
fragments.push(node.fragment);
break;
@ -1234,7 +1238,7 @@ function loop_child(children, direction, adjacent_only, seen) {
/**
* @param {Compiler.AST.SvelteNode} node
* @returns {node is Compiler.AST.IfBlock | Compiler.AST.EachBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.SlotElement}
* @returns {node is Compiler.AST.IfBlock | Compiler.AST.EachBlock | Compiler.AST.AwaitBlock | Compiler.AST.KeyBlock | Compiler.AST.PortalBlock | Compiler.AST.SlotElement}
*/
function is_block(node) {
return (
@ -1242,6 +1246,7 @@ function is_block(node) {
node.type === 'EachBlock' ||
node.type === 'AwaitBlock' ||
node.type === 'KeyBlock' ||
node.type === 'PortalBlock' ||
node.type === 'SlotElement'
);
}

@ -58,6 +58,8 @@ import { Literal } from './visitors/Literal.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { NewExpression } from './visitors/NewExpression.js';
import { OnDirective } from './visitors/OnDirective.js';
import { PortalBlock } from './visitors/PortalBlock.js';
import { PortalTag } from './visitors/PortalTag.js';
import { PropertyDefinition } from './visitors/PropertyDefinition.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
@ -75,7 +77,6 @@ import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { SveltePortal } from './visitors/SveltePortal.js';
import { TaggedTemplateExpression } from './visitors/TaggedTemplateExpression.js';
import { TemplateElement } from './visitors/TemplateElement.js';
import { Text } from './visitors/Text.js';
@ -181,6 +182,8 @@ const visitors = {
MemberExpression,
NewExpression,
OnDirective,
PortalBlock,
PortalTag,
PropertyDefinition,
RegularElement,
RenderTag,
@ -198,7 +201,6 @@ const visitors = {
SvelteSelf,
SvelteWindow,
SvelteBoundary,
SveltePortal,
TaggedTemplateExpression,
TemplateElement,
Text,

@ -27,6 +27,7 @@ export function ConstTag(node, context) {
grand_parent?.type !== 'SnippetBlock' &&
grand_parent?.type !== 'SvelteBoundary' &&
grand_parent?.type !== 'KeyBlock' &&
grand_parent?.type !== 'PortalBlock' &&
((grand_parent?.type !== 'RegularElement' && grand_parent?.type !== 'SvelteElement') ||
!grand_parent.attributes.some((a) => a.type === 'Attribute' && a.name === 'slot')))
) {

@ -0,0 +1,18 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.PortalBlock} node
* @param {Context} context
*/
export function PortalBlock(node, context) {
mark_subtree_dynamic(context.path);
context.visit(node.expression, {
...context.state,
expression: node.metadata.expression
});
context.visit(node.fragment);
}

@ -0,0 +1,19 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { validate_opening_tag } from './shared/utils.js';
import { mark_subtree_dynamic } from './shared/fragment.js';
/**
* @param {AST.PortalTag} node
* @param {Context} context
*/
export function PortalTag(node, context) {
validate_opening_tag(node, context.state, '@');
node.metadata.path = [...context.path];
mark_subtree_dynamic(context.path);
context.visit(node.expression, {
...context.state,
expression: node.metadata.expression
});
}

@ -1,15 +0,0 @@
/** @import { AST } from '#compiler' */
/** @import { Context } from '../types' */
import { visit_component } from './shared/component.js';
import * as e from '../../../errors.js';
import * as w from '../../../warnings.js';
import { filename } from '../../../state.js';
/**
* @param {AST.SveltePortal} node
* @param {Context} context
*/
export function SveltePortal(node, context) {
// TODO validation for attributes etc
context.next();
}

@ -39,6 +39,8 @@ import { LabeledStatement } from './visitors/LabeledStatement.js';
import { LetDirective } from './visitors/LetDirective.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { OnDirective } from './visitors/OnDirective.js';
import { PortalBlock } from './visitors/PortalBlock.js';
import { PortalTag } from './visitors/PortalTag.js';
import { Program } from './visitors/Program.js';
import { RegularElement } from './visitors/RegularElement.js';
import { RenderTag } from './visitors/RenderTag.js';
@ -51,7 +53,6 @@ import { SvelteDocument } from './visitors/SvelteDocument.js';
import { SvelteElement } from './visitors/SvelteElement.js';
import { SvelteFragment } from './visitors/SvelteFragment.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { SveltePortal } from './visitors/SveltePortal.js';
import { SvelteHead } from './visitors/SvelteHead.js';
import { SvelteSelf } from './visitors/SvelteSelf.js';
import { SvelteWindow } from './visitors/SvelteWindow.js';
@ -105,6 +106,8 @@ const visitors = {
LetDirective,
MemberExpression,
OnDirective,
PortalBlock,
PortalTag,
Program,
RegularElement,
RenderTag,
@ -117,7 +120,6 @@ const visitors = {
SvelteElement,
SvelteFragment,
SvelteBoundary,
SveltePortal,
SvelteHead,
SvelteSelf,
SvelteWindow,

@ -0,0 +1,23 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.PortalBlock} node
* @param {ComponentContext} context
*/
export function PortalBlock(node, context) {
context.state.template.push_comment();
const value = build_expression(context, node.expression, node.metadata.expression);
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(node.metadata.expression.has_state ? b.call('$.render_effect', b.thunk(portal)) : portal)
);
}

@ -0,0 +1,15 @@
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '#compiler/builders';
import { build_expression } from './shared/utils.js';
/**
* @param {AST.PortalTag} node
* @param {ComponentContext} context
*/
export function PortalTag(node, context) {
context.state.template.push_comment();
const value = build_expression(context, node.expression, node.metadata.expression);
context.state.init.push(b.stmt(b.call('$.portal_outlet', context.state.node, b.thunk(value))));
}

@ -1,36 +0,0 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';
import { build_attribute_value } from './shared/element.js';
/**
* @param {AST.SveltePortal} node
* @param {ComponentContext} context
*/
export function SveltePortal(node, context) {
const _for = node.attributes.find((attr) => attr.type === 'Attribute' && attr.name === 'for');
const target = node.attributes.find(
(attr) => attr.type === 'Attribute' && attr.name === 'target'
);
context.state.template.push_comment();
if (target) {
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(has_state ? b.call('$.render_effect', b.thunk(portal)) : portal)
);
} else {
// TODO reactive sources? Doesn't really make sense IMHO
const value = build_attribute_value(/** @type {AST.Attribute} */ (_for).value, context);
context.state.init.push(b.stmt(b.call('$.portal_outlet', context.state.node, value.value)));
}
}

@ -26,6 +26,8 @@ import { IfBlock } from './visitors/IfBlock.js';
import { KeyBlock } from './visitors/KeyBlock.js';
import { LabeledStatement } from './visitors/LabeledStatement.js';
import { MemberExpression } from './visitors/MemberExpression.js';
import { PortalBlock } from './visitors/PortalBlock.js';
import { PortalTag } from './visitors/PortalTag.js';
import { Program } from './visitors/Program.js';
import { PropertyDefinition } from './visitors/PropertyDefinition.js';
import { RegularElement } from './visitors/RegularElement.js';
@ -42,7 +44,6 @@ import { TitleElement } from './visitors/TitleElement.js';
import { UpdateExpression } from './visitors/UpdateExpression.js';
import { VariableDeclaration } from './visitors/VariableDeclaration.js';
import { SvelteBoundary } from './visitors/SvelteBoundary.js';
import { SveltePortal } from './visitors/SveltePortal.js';
/** @type {Visitors} */
const global_visitors = {
@ -73,6 +74,8 @@ const template_visitors = {
HtmlTag,
IfBlock,
KeyBlock,
PortalBlock,
PortalTag,
RegularElement,
RenderTag,
SlotElement,
@ -84,8 +87,7 @@ const template_visitors = {
SvelteHead,
SvelteSelf,
TitleElement,
SvelteBoundary,
SveltePortal
SvelteBoundary
};
/**

@ -0,0 +1,17 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
/**
* @param {AST.PortalBlock} node
* @param {ComponentContext} context
*/
export function PortalBlock(node, context) {
const value = /** @type {import('estree').Expression} */ (context.visit(node.expression));
const body = /** @type {BlockStatement} */ (context.visit(node.fragment, context.state));
context.state.template.push(
b.stmt(b.call('$.portal', b.id('$$renderer'), value, b.arrow([b.id('$$renderer')], body)))
);
}

@ -0,0 +1,21 @@
/** @import { Expression } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '#compiler/builders';
import { PromiseOptimiser } from './shared/utils.js';
/**
* @param {AST.PortalTag} node
* @param {ComponentContext} context
*/
export function PortalTag(node, context) {
const optimiser = new PromiseOptimiser();
const value = optimiser.transform(
/** @type {Expression} */ (context.visit(node.expression)),
node.metadata.expression
);
context.state.template.push(
...optimiser.render_block([b.stmt(b.call('$.portal_outlet', b.id('$$renderer'), value))])
);
}

@ -1,35 +0,0 @@
/** @import { BlockStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import * as b from '../../../../utils/builders.js';
import { build_attribute_value } from './shared/utils.js';
/**
* @param {AST.SveltePortal} node
* @param {ComponentContext} context
*/
export function SveltePortal(node, context) {
const _for = node.attributes.find((attr) => attr.type === 'Attribute' && attr.name === 'for');
const target = node.attributes.find(
(attr) => attr.type === 'Attribute' && attr.name === 'target'
);
if (target) {
const value = build_attribute_value(
/** @type {AST.Attribute} */ (target).value,
context,
(expression) => expression
);
const body = /** @type {BlockStatement} */ (context.visit(node.fragment, context.state));
context.state.template.push(
b.stmt(b.call('$.portal', b.id('$$renderer'), value, b.arrow([b.id('$$renderer')], body)))
);
} else {
const value = build_attribute_value(
/** @type {AST.Attribute} */ (_for).value,
context,
(expression) => expression
);
context.state.template.push(b.stmt(b.call('$.portal_outlet', b.id('$$renderer'), value)));
}
}

@ -296,6 +296,7 @@ export function clean_nodes(
(parent.type === 'Fragment' ||
parent.type === 'SnippetBlock' ||
parent.type === 'EachBlock' ||
parent.type === 'PortalBlock' ||
parent.type === 'SvelteComponent' ||
parent.type === 'SvelteBoundary' ||
parent.type === 'Component' ||
@ -331,6 +332,7 @@ export function infer_namespace(namespace, parent, nodes) {
parent.type === 'SvelteComponent' ||
parent.type === 'SvelteFragment' ||
parent.type === 'SnippetBlock' ||
parent.type === 'PortalBlock' ||
parent.type === 'SlotElement'
) {
const new_namespace = check_nodes_for_namespace(nodes, 'keep');
@ -392,6 +394,7 @@ function check_nodes_for_namespace(nodes, namespace) {
node.type === 'AwaitBlock' ||
node.type === 'Fragment' ||
node.type === 'KeyBlock' ||
node.type === 'PortalBlock' ||
node.type === 'RegularElement' ||
node.type === 'SvelteElement' ||
node.type === 'Text'

@ -242,6 +242,10 @@ function* find_descendants(fragment) {
yield* find_descendants(node.fragment);
break;
case 'PortalBlock':
yield* find_descendants(node.fragment);
break;
case 'AwaitBlock':
yield* find_descendants(node.pending);
yield* find_descendants(node.then);

@ -771,6 +771,20 @@ const svelte_visitors = (comments) => ({
}
},
PortalBlock(node, context) {
context.write('{#portal ');
context.visit(node.expression);
context.write('}');
block(context, node.fragment);
context.write('{/portal}');
},
PortalTag(node, context) {
context.write('{@portal ');
context.visit(node.expression);
context.write('}');
},
RegularElement(node, context) {
base_element(node, context, comments);
},

@ -194,6 +194,17 @@ export namespace AST {
};
}
/** A `{@portal foo}` tag */
export interface PortalTag extends BaseNode {
type: 'PortalTag';
expression: Expression;
/** @internal */
metadata: {
expression: ExpressionMetadata;
path: SvelteNode[];
};
}
/** A `{@attach foo(...)} tag */
export interface AttachTag extends BaseNode {
type: 'AttachTag';
@ -430,11 +441,6 @@ export namespace AST {
name: 'svelte:boundary';
}
export interface SveltePortal extends BaseElement {
type: 'SveltePortal';
name: 'svelte:portal';
}
export interface SvelteHead extends BaseElement {
type: 'SvelteHead';
name: 'svelte:head';
@ -538,6 +544,16 @@ export namespace AST {
};
}
export interface PortalBlock extends BaseNode {
type: 'PortalBlock';
expression: Expression;
fragment: Fragment;
/** @internal */
metadata: {
expression: ExpressionMetadata;
};
}
export interface SnippetBlock extends BaseNode {
type: 'SnippetBlock';
expression: Identifier;
@ -617,6 +633,7 @@ export namespace AST {
| AST.IfBlock
| AST.AwaitBlock
| AST.KeyBlock
| AST.PortalBlock
| AST.SnippetBlock;
export type ElementLike =
@ -634,8 +651,7 @@ export namespace AST {
| AST.SvelteOptionsRaw
| AST.SvelteSelf
| AST.SvelteWindow
| AST.SvelteBoundary
| AST.SveltePortal;
| AST.SvelteBoundary;
export type Tag =
| AST.AttachTag
@ -644,6 +660,7 @@ export namespace AST {
| AST.DebugTag
| AST.ExpressionTag
| AST.HtmlTag
| AST.PortalTag
| AST.RenderTag;
export type TemplateNode =

@ -9,13 +9,15 @@ const portals = new Map();
/**
* @param {TemplateNode} node
* @param {any} id
* @param {any | (() => any)} id
* @returns {void}
*/
export function portal_outlet(node, id) {
var anchor = node;
const get_id = typeof id === 'function' ? id : () => id;
render_effect(() => {
id = get_id();
portals.set(id, { anchor });
return () => {
@ -45,6 +47,7 @@ export function portal_outlet(node, id) {
* @returns {void}
*/
export function portal(target, content) {
console.log('huh', target, content);
if (target == null) return;
const is_dom_node = target instanceof Element;

@ -682,8 +682,8 @@ export class Renderer {
if (item instanceof Renderer) {
const portal_content = item.#collect_content();
item.#out.length = 0;
content.body.replace(id, id + portal_content.body);
content.head.replace(id, id + portal_content.body);
content.body = content.body.replace(id, id + portal_content.body);
content.head = content.head.replace(id, id + portal_content.body);
}
}
}

@ -6,7 +6,7 @@ export class PortalKey {
}
/**
* Creates a key for use with a `<svelte:portal>`. It connects the portal source and target.
* Creates a key for use with `{#portal ...}` and `{@portal ...}`. It connects the portal source and outlet.
* Example: TODO write out once exact API clear.
* @param {string} name
*/

@ -0,0 +1,11 @@
<script>
import { createPortalKey } from 'svelte';
const key = createPortalKey('example');
</script>
{@portal key}
{#portal key}
<p>portaled</p>
{/portal}

@ -0,0 +1,327 @@
{
"css": null,
"js": [],
"start": 0,
"end": 160,
"type": "Root",
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 103,
"end": 105,
"raw": "\n\n",
"data": "\n\n"
},
{
"type": "PortalTag",
"start": 105,
"end": 118,
"expression": {
"type": "Identifier",
"start": 114,
"end": 117,
"loc": {
"start": {
"line": 7,
"column": 9
},
"end": {
"line": 7,
"column": 12
}
},
"name": "key"
}
},
{
"type": "Text",
"start": 118,
"end": 120,
"raw": "\n\n",
"data": "\n\n"
},
{
"type": "PortalBlock",
"start": 120,
"end": 160,
"expression": {
"type": "Identifier",
"start": 129,
"end": 132,
"loc": {
"start": {
"line": 9,
"column": 9
},
"end": {
"line": 9,
"column": 12
}
},
"name": "key"
},
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 133,
"end": 135,
"raw": "\n\t",
"data": "\n\t"
},
{
"type": "RegularElement",
"start": 135,
"end": 150,
"name": "p",
"name_loc": {
"start": {
"line": 10,
"column": 2,
"character": 136
},
"end": {
"line": 10,
"column": 3,
"character": 137
}
},
"attributes": [],
"fragment": {
"type": "Fragment",
"nodes": [
{
"type": "Text",
"start": 138,
"end": 146,
"raw": "portaled",
"data": "portaled"
}
]
}
},
{
"type": "Text",
"start": 150,
"end": 151,
"raw": "\n",
"data": "\n"
}
]
}
}
]
},
"options": null,
"comments": [],
"instance": {
"type": "Script",
"start": 0,
"end": 103,
"context": "default",
"content": {
"type": "Program",
"start": 8,
"end": 94,
"loc": {
"start": {
"line": 1,
"column": 0
},
"end": {
"line": 5,
"column": 9
}
},
"body": [
{
"type": "ImportDeclaration",
"start": 10,
"end": 51,
"loc": {
"start": {
"line": 2,
"column": 1
},
"end": {
"line": 2,
"column": 42
}
},
"specifiers": [
{
"type": "ImportSpecifier",
"start": 19,
"end": 34,
"loc": {
"start": {
"line": 2,
"column": 10
},
"end": {
"line": 2,
"column": 25
}
},
"imported": {
"type": "Identifier",
"start": 19,
"end": 34,
"loc": {
"start": {
"line": 2,
"column": 10
},
"end": {
"line": 2,
"column": 25
}
},
"name": "createPortalKey"
},
"local": {
"type": "Identifier",
"start": 19,
"end": 34,
"loc": {
"start": {
"line": 2,
"column": 10
},
"end": {
"line": 2,
"column": 25
}
},
"name": "createPortalKey"
}
}
],
"source": {
"type": "Literal",
"start": 42,
"end": 50,
"loc": {
"start": {
"line": 2,
"column": 33
},
"end": {
"line": 2,
"column": 41
}
},
"value": "svelte",
"raw": "'svelte'"
},
"attributes": []
},
{
"type": "VariableDeclaration",
"start": 54,
"end": 93,
"loc": {
"start": {
"line": 4,
"column": 1
},
"end": {
"line": 4,
"column": 40
}
},
"declarations": [
{
"type": "VariableDeclarator",
"start": 60,
"end": 92,
"loc": {
"start": {
"line": 4,
"column": 7
},
"end": {
"line": 4,
"column": 39
}
},
"id": {
"type": "Identifier",
"start": 60,
"end": 63,
"loc": {
"start": {
"line": 4,
"column": 7
},
"end": {
"line": 4,
"column": 10
}
},
"name": "key"
},
"init": {
"type": "CallExpression",
"start": 66,
"end": 92,
"loc": {
"start": {
"line": 4,
"column": 13
},
"end": {
"line": 4,
"column": 39
}
},
"callee": {
"type": "Identifier",
"start": 66,
"end": 81,
"loc": {
"start": {
"line": 4,
"column": 13
},
"end": {
"line": 4,
"column": 28
}
},
"name": "createPortalKey"
},
"arguments": [
{
"type": "Literal",
"start": 82,
"end": 91,
"loc": {
"start": {
"line": 4,
"column": 29
},
"end": {
"line": 4,
"column": 38
}
},
"value": "example",
"raw": "'example'"
}
],
"optional": false
}
}
],
"kind": "const"
}
],
"sourceType": "module"
},
"attributes": []
}
}

@ -0,0 +1,6 @@
import { test } from '../../test';
export default test({
mode: ['client'],
html: `<main><p>portaled</p></main>`
});

@ -0,0 +1,12 @@
<script>
import { createPortalKey } from 'svelte';
const key = createPortalKey('example');
let target = $state(null);
</script>
<main bind:this={target}></main>
{#portal target}
<p>portaled</p>
{/portal}

@ -0,0 +1,5 @@
import { test } from '../../test';
export default test({
html: `<main><p>portaled</p></main>`
});

@ -0,0 +1,13 @@
<script>
import { createPortalKey } from 'svelte';
const key = createPortalKey('example');
</script>
<main>
{@portal key}
</main>
{#portal key}
<p>portaled</p>
{/portal}

@ -466,6 +466,16 @@ declare module 'svelte' {
render: () => string;
setup?: (element: Element) => void | (() => void);
}): Snippet<Params>;
/**
* Creates a key for use with `{#portal ...}` and `{@portal ...}`. It connects the portal source and outlet.
* Example: TODO write out once exact API clear.
* */
export function createPortalKey(name: string): PortalKey;
class PortalKey {
constructor(name: string);
v: string;
}
/** Anything except a function */
type NotFunction<T> = T extends Function ? never : T;
/**
@ -1321,6 +1331,12 @@ declare module 'svelte/compiler' {
expression: SimpleCallExpression | (ChainExpression & { expression: SimpleCallExpression });
}
/** A `{@portal foo}` tag */
export interface PortalTag extends BaseNode {
type: 'PortalTag';
expression: Expression;
}
/** A `{@attach foo(...)} tag */
export interface AttachTag extends BaseNode {
type: 'AttachTag';
@ -1536,6 +1552,12 @@ declare module 'svelte/compiler' {
fragment: Fragment;
}
export interface PortalBlock extends BaseNode {
type: 'PortalBlock';
expression: Expression;
fragment: Fragment;
}
export interface SnippetBlock extends BaseNode {
type: 'SnippetBlock';
expression: Identifier;
@ -1597,6 +1619,7 @@ declare module 'svelte/compiler' {
| AST.IfBlock
| AST.AwaitBlock
| AST.KeyBlock
| AST.PortalBlock
| AST.SnippetBlock;
export type ElementLike =
@ -1623,6 +1646,7 @@ declare module 'svelte/compiler' {
| AST.DebugTag
| AST.ExpressionTag
| AST.HtmlTag
| AST.PortalTag
| AST.RenderTag;
export type TemplateNode =

Loading…
Cancel
Save