Skip to content

Commit

Permalink
dynamic elements
Browse files Browse the repository at this point in the history
  • Loading branch information
Rich-Harris committed Jan 24, 2025
1 parent 1426a6d commit 8a28f72
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 16 deletions.
2 changes: 2 additions & 0 deletions packages/svelte/src/compiler/phases/1-parse/state/element.js
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ export default function element(parser) {
} else {
element.tag = get_attribute_expression(definition);
}

element.metadata.expression = create_expression_metadata();
}

if (is_top_level_script_or_style) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,17 @@ export function SvelteElement(node, context) {

mark_subtree_dynamic(context.path);

context.next({ ...context.state, parent_element: null });
context.visit(node.tag, {
...context.state,
expression: node.metadata.expression
});

for (const attribute of node.attributes) {
context.visit(attribute);
}

context.visit(node.fragment, {
...context.state,
parent_element: null
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function SvelteElement(node, context) {
const style_directives = [];

/** @type {ExpressionStatement[]} */
const lets = [];
const statements = [];

// Create a temporary context which picks up the init/update statements.
// They'll then be added to the function parameter of $.element
Expand Down Expand Up @@ -66,7 +66,7 @@ export function SvelteElement(node, context) {
} else if (attribute.type === 'StyleDirective') {
style_directives.push(attribute);
} else if (attribute.type === 'LetDirective') {
lets.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
statements.push(/** @type {ExpressionStatement} */ (context.visit(attribute)));
} else if (attribute.type === 'OnDirective') {
const handler = /** @type {Expression} */ (context.visit(attribute, inner_context.state));
inner_context.state.after_update.push(b.stmt(handler));
Expand All @@ -75,9 +75,6 @@ export function SvelteElement(node, context) {
}
}

// Let bindings first, they can be used on attributes
context.state.init.push(...lets); // create computeds in the outer context; the dynamic element is the single child of this slot

// Then do attributes
let is_attributes_reactive = false;

Expand Down Expand Up @@ -108,15 +105,6 @@ export function SvelteElement(node, context) {
build_class_directives(class_directives, element_id, inner_context, is_attributes_reactive);
build_style_directives(style_directives, element_id, inner_context, is_attributes_reactive);

const get_tag = b.thunk(/** @type {Expression} */ (context.visit(node.tag)));

if (dev) {
if (node.fragment.nodes.length > 0) {
context.state.init.push(b.stmt(b.call('$.validate_void_dynamic_element', get_tag)));
}
context.state.init.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag)));
}

/** @type {Statement[]} */
const inner = inner_context.state.init;
if (inner_context.state.update.length > 0) {
Expand All @@ -135,9 +123,21 @@ export function SvelteElement(node, context) {
).body
);

const { is_async } = node.metadata.expression;

const expression = /** @type {Expression} */ (context.visit(node.tag));
const get_tag = b.thunk(is_async ? b.call('$.get', b.id('$$tag')) : expression);

if (dev) {
if (node.fragment.nodes.length > 0) {
statements.push(b.stmt(b.call('$.validate_void_dynamic_element', get_tag)));
}
statements.push(b.stmt(b.call('$.validate_dynamic_element_tag', get_tag)));
}

const location = dev && locator(node.start);

context.state.init.push(
statements.push(
b.stmt(
b.call(
'$.element',
Expand All @@ -150,4 +150,19 @@ export function SvelteElement(node, context) {
)
)
);

if (is_async) {
context.state.init.push(
b.stmt(
b.call(
'$.async',
context.state.node,
b.array([b.thunk(expression, true)]),
b.arrow([context.state.node, b.id('$$tag')], b.block(statements))
)
)
);
} else {
context.state.init.push(statements.length === 1 ? statements[0] : b.block(statements));
}
}
1 change: 1 addition & 0 deletions packages/svelte/src/compiler/types/template.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ export namespace AST {
tag: Expression;
/** @internal */
metadata: {
expression: ExpressionMetadata;
/**
* `true` if this is an svg element. The boolean may not be accurate because
* the tag is dynamic, but we do our best to infer it from the template.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { flushSync, tick } from 'svelte';
import { deferred } from '../../../../src/internal/shared/utils.js';
import { test } from '../../test';

/** @type {ReturnType<typeof deferred>} */
let d;

export default test({
html: `<p>pending</p>`,

get props() {
d = deferred();

return {
promise: d.promise
};
},

async test({ assert, target, component }) {
d.resolve('h1');
await Promise.resolve();
await Promise.resolve();
await tick();
flushSync();
assert.htmlEqual(target.innerHTML, '<h1>hello</h1>');

component.promise = (d = deferred()).promise;
await tick();
assert.htmlEqual(target.innerHTML, '<p>pending</p>');

d.resolve('h2');
await tick();
assert.htmlEqual(target.innerHTML, '<h2>hello</h2>');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script>
let { promise } = $props();
</script>

<svelte:boundary>
<svelte:element this={await promise}>hello</svelte:element>

{#snippet pending()}
<p>pending</p>
{/snippet}
</svelte:boundary>

0 comments on commit 8a28f72

Please sign in to comment.