Skip to content

Commit e0d9325

Browse files
baseballyamaAlfred RingstadSimon Holthausentanhauhau
authored
[feature] Dynamic elements implementation <svelte:element> (#6898)
Closes #2324 Co-authored-by: Alfred Ringstad <[email protected]> Co-authored-by: Simon Holthausen <[email protected]> Co-authored-by: tanhauhau <[email protected]>
1 parent 54197c5 commit e0d9325

File tree

99 files changed

+1170
-22
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

99 files changed

+1170
-22
lines changed

site/content/docs/02-template-syntax.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1627,6 +1627,28 @@ If `this` is falsy, no component is rendered.
16271627
<svelte:component this={currentSelection.component} foo={bar}/>
16281628
```
16291629

1630+
### `<svelte:element>`
1631+
1632+
```sv
1633+
<svelte:element this={expression}/>
1634+
```
1635+
1636+
---
1637+
1638+
The `<svelte:element>` element lets you render an element of a dynamically specified type. This is useful for example when rich text content from a CMS. If the tag is changed, the children will be preserved unless there's a transition attached to the element. Any properties and event listeners present will be applied to the element.
1639+
1640+
The only supported binding is `bind:this`, since the element type specific bindings that Svelte does at build time (e.g. `bind:value` for input elements) does not work with a dynamic tag type.
1641+
1642+
If `this` has a nullish value, a warning will be logged in development mode.
1643+
1644+
```sv
1645+
<script>
1646+
let tag = 'div';
1647+
export let handler;
1648+
</script>
1649+
1650+
<svelte:element this={tag} on:click={handler}>Foo</svelte:element>
1651+
```
16301652

16311653
### `<svelte:window>`
16321654

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<script>
2+
const options = ['h1', 'h3', 'p'];
3+
let selected = options[0];
4+
</script>
5+
6+
<select bind:value={selected}>
7+
{#each options as option}
8+
<option value={option}>{option}</option>
9+
{/each}
10+
</select>
11+
12+
{#if selected === 'h1'}
13+
<h1>I'm a h1 tag</h1>
14+
{:else if selected === 'h3'}
15+
<h3>I'm a h3 tag</h3>
16+
{:else if selected === 'p'}
17+
<p>I'm a p tag</p>
18+
{/if}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<script>
2+
const options = ['h1', 'h3', 'p'];
3+
let selected = options[0];
4+
</script>
5+
6+
<select bind:value={selected}>
7+
{#each options as option}
8+
<option value={option}>{option}</option>
9+
{/each}
10+
</select>
11+
12+
<svelte:element this={selected}>I'm a {selected} tag</svelte:element>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
title: <svelte:element>
3+
---
4+
5+
Sometimes we don't know in advance what kind of DOM element to render. `<svelte:element>` comes in handy here. Instead of a sequence of `if` blocks...
6+
7+
```html
8+
{#if selected === 'h1'}
9+
<h1>I'm a h1 tag</h1>
10+
{:else if selected === 'h3'}
11+
<h3>I'm a h3 tag</h3>
12+
{:else if selected === 'p'}
13+
<p>I'm a p tag</p>
14+
{/if}
15+
```
16+
17+
...we can have a single dynamic component:
18+
19+
```html
20+
<svelte:element this={selected}>I'm a {selected} tag</svelte:element>
21+
```
22+
23+
The `this` value can be any string, or a falsy value — if it's falsy, no element is rendered.

src/compiler/compile/compiler_errors.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ export default {
246246
code: 'invalid-animation',
247247
message: 'An element that uses the animate directive must be the sole child of a keyed each block'
248248
},
249+
invalid_animation_dynamic_element: {
250+
code: 'invalid-animation',
251+
message: '<svelte:element> cannot have a animate directive'
252+
},
249253
invalid_directive_value: {
250254
code: 'invalid-directive-value',
251255
message: 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'

src/compiler/compile/nodes/Element.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import Let from './Let';
1818
import TemplateScope from './shared/TemplateScope';
1919
import { INode } from './interfaces';
2020
import Component from '../Component';
21+
import Expression from './shared/Expression';
22+
import { string_literal } from '../utils/stringify';
23+
import { Literal } from 'estree';
2124
import compiler_warnings from '../compiler_warnings';
2225
import compiler_errors from '../compiler_errors';
2326

@@ -190,11 +193,26 @@ export default class Element extends Node {
190193
children: INode[];
191194
namespace: string;
192195
needs_manual_style_scoping: boolean;
196+
tag_expr: Expression;
197+
198+
get is_dynamic_element() {
199+
return this.name === 'svelte:element';
200+
}
193201

194202
constructor(component: Component, parent: Node, scope: TemplateScope, info: any) {
195203
super(component, parent, scope, info);
196204
this.name = info.name;
197205

206+
if (info.name === 'svelte:element') {
207+
if (typeof info.tag !== 'string') {
208+
this.tag_expr = new Expression(component, this, scope, info.tag);
209+
} else {
210+
this.tag_expr = new Expression(component, this, scope, string_literal(info.tag) as Literal);
211+
}
212+
} else {
213+
this.tag_expr = new Expression(component, this, scope, string_literal(this.name) as Literal);
214+
}
215+
198216
this.namespace = get_namespace(parent as Element, this, component.namespace);
199217

200218
if (this.namespace !== namespaces.foreign) {

src/compiler/compile/render_dom/Block.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export default class Block {
4848
hydrate: Array<Node | Node[]>;
4949
mount: Array<Node | Node[]>;
5050
measure: Array<Node | Node[]>;
51+
restore_measurements: Array<Node | Node[]>;
5152
fix: Array<Node | Node[]>;
5253
animate: Array<Node | Node[]>;
5354
intro: Array<Node | Node[]>;
@@ -96,6 +97,7 @@ export default class Block {
9697
hydrate: [],
9798
mount: [],
9899
measure: [],
100+
restore_measurements: [],
99101
fix: [],
100102
animate: [],
101103
intro: [],
@@ -326,6 +328,12 @@ export default class Block {
326328
${this.chunks.measure}
327329
}`;
328330

331+
if (this.chunks.restore_measurements.length) {
332+
properties.restore_measurements = x`function #restore_measurements(#measurement) {
333+
${this.chunks.restore_measurements}
334+
}`;
335+
}
336+
329337
properties.fix = x`function #fix() {
330338
${this.chunks.fix}
331339
}`;
@@ -379,6 +387,7 @@ export default class Block {
379387
m: ${properties.mount},
380388
p: ${properties.update},
381389
r: ${properties.measure},
390+
s: ${properties.restore_measurements},
382391
f: ${properties.fix},
383392
a: ${properties.animate},
384393
i: ${properties.intro},

0 commit comments

Comments
 (0)