Skip to content

Commit

Permalink
[feature] Dynamic elements implementation <svelte:element> (#6898)
Browse files Browse the repository at this point in the history
Closes #2324

Co-authored-by: Alfred Ringstad <alfred.ringstad@hyperlab.se>
Co-authored-by: Simon Holthausen <simon.holthausen@accso.de>
Co-authored-by: tanhauhau <lhtan93@gmail.com>
  • Loading branch information
4 people committed Apr 8, 2022
1 parent 54197c5 commit e0d9325
Show file tree
Hide file tree
Showing 99 changed files with 1,170 additions and 22 deletions.
22 changes: 22 additions & 0 deletions site/content/docs/02-template-syntax.md
Expand Up @@ -1627,6 +1627,28 @@ If `this` is falsy, no component is rendered.
<svelte:component this={currentSelection.component} foo={bar}/>
```

### `<svelte:element>`

```sv
<svelte:element this={expression}/>
```

---

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.

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.

If `this` has a nullish value, a warning will be logged in development mode.

```sv
<script>
let tag = 'div';
export let handler;
</script>
<svelte:element this={tag} on:click={handler}>Foo</svelte:element>
```

### `<svelte:window>`

Expand Down
@@ -0,0 +1,18 @@
<script>
const options = ['h1', 'h3', 'p'];
let selected = options[0];
</script>

<select bind:value={selected}>
{#each options as option}
<option value={option}>{option}</option>
{/each}
</select>

{#if selected === 'h1'}
<h1>I'm a h1 tag</h1>
{:else if selected === 'h3'}
<h3>I'm a h3 tag</h3>
{:else if selected === 'p'}
<p>I'm a p tag</p>
{/if}
@@ -0,0 +1,12 @@
<script>
const options = ['h1', 'h3', 'p'];
let selected = options[0];
</script>

<select bind:value={selected}>
{#each options as option}
<option value={option}>{option}</option>
{/each}
</select>

<svelte:element this={selected}>I'm a {selected} tag</svelte:element>
@@ -0,0 +1,23 @@
---
title: <svelte:element>
---

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...

```html
{#if selected === 'h1'}
<h1>I'm a h1 tag</h1>
{:else if selected === 'h3'}
<h3>I'm a h3 tag</h3>
{:else if selected === 'p'}
<p>I'm a p tag</p>
{/if}
```

...we can have a single dynamic component:

```html
<svelte:element this={selected}>I'm a {selected} tag</svelte:element>
```

The `this` value can be any string, or a falsy value — if it's falsy, no element is rendered.
4 changes: 4 additions & 0 deletions src/compiler/compile/compiler_errors.ts
Expand Up @@ -246,6 +246,10 @@ export default {
code: 'invalid-animation',
message: 'An element that uses the animate directive must be the sole child of a keyed each block'
},
invalid_animation_dynamic_element: {
code: 'invalid-animation',
message: '<svelte:element> cannot have a animate directive'
},
invalid_directive_value: {
code: 'invalid-directive-value',
message: 'Can only bind to an identifier (e.g. `foo`) or a member expression (e.g. `foo.bar` or `foo[baz]`)'
Expand Down
18 changes: 18 additions & 0 deletions src/compiler/compile/nodes/Element.ts
Expand Up @@ -18,6 +18,9 @@ import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import Component from '../Component';
import Expression from './shared/Expression';
import { string_literal } from '../utils/stringify';
import { Literal } from 'estree';
import compiler_warnings from '../compiler_warnings';
import compiler_errors from '../compiler_errors';

Expand Down Expand Up @@ -190,11 +193,26 @@ export default class Element extends Node {
children: INode[];
namespace: string;
needs_manual_style_scoping: boolean;
tag_expr: Expression;

get is_dynamic_element() {
return this.name === 'svelte:element';
}

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

if (info.name === 'svelte:element') {
if (typeof info.tag !== 'string') {
this.tag_expr = new Expression(component, this, scope, info.tag);
} else {
this.tag_expr = new Expression(component, this, scope, string_literal(info.tag) as Literal);
}
} else {
this.tag_expr = new Expression(component, this, scope, string_literal(this.name) as Literal);
}

this.namespace = get_namespace(parent as Element, this, component.namespace);

if (this.namespace !== namespaces.foreign) {
Expand Down
9 changes: 9 additions & 0 deletions src/compiler/compile/render_dom/Block.ts
Expand Up @@ -48,6 +48,7 @@ export default class Block {
hydrate: Array<Node | Node[]>;
mount: Array<Node | Node[]>;
measure: Array<Node | Node[]>;
restore_measurements: Array<Node | Node[]>;
fix: Array<Node | Node[]>;
animate: Array<Node | Node[]>;
intro: Array<Node | Node[]>;
Expand Down Expand Up @@ -96,6 +97,7 @@ export default class Block {
hydrate: [],
mount: [],
measure: [],
restore_measurements: [],
fix: [],
animate: [],
intro: [],
Expand Down Expand Up @@ -326,6 +328,12 @@ export default class Block {
${this.chunks.measure}
}`;

if (this.chunks.restore_measurements.length) {
properties.restore_measurements = x`function #restore_measurements(#measurement) {
${this.chunks.restore_measurements}
}`;
}

properties.fix = x`function #fix() {
${this.chunks.fix}
}`;
Expand Down Expand Up @@ -379,6 +387,7 @@ export default class Block {
m: ${properties.mount},
p: ${properties.update},
r: ${properties.measure},
s: ${properties.restore_measurements},
f: ${properties.fix},
a: ${properties.animate},
i: ${properties.intro},
Expand Down

0 comments on commit e0d9325

Please sign in to comment.