Skip to content

Commit fe4158a

Browse files
author
Alfred Ringstad
committed
Implement svelte:element for dynamically setting HTML DOM type
1 parent 324f74b commit fe4158a

File tree

30 files changed

+680
-9
lines changed

30 files changed

+680
-9
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import Node from './shared/Node';
2+
import Attribute from './Attribute';
3+
import Binding from './Binding';
4+
import EventHandler from './EventHandler';
5+
import Let from './Let';
6+
import TemplateScope from './shared/TemplateScope';
7+
import { INode } from './interfaces';
8+
import Expression from './shared/Expression';
9+
import Component from '../Component';
10+
import map_children from './shared/map_children';
11+
import Class from './Class';
12+
import Transition from './Transition';
13+
import Animation from './Animation';
14+
import Action from './Action';
15+
import { string_literal } from '../utils/stringify';
16+
17+
export default class DynamicElement extends Node {
18+
type: 'DynamicElement';
19+
name: string;
20+
tag: Expression;
21+
attributes: Attribute[] = [];
22+
actions: Action[] = [];
23+
bindings: Binding[] = [];
24+
classes: Class[] = [];
25+
handlers: EventHandler[] = [];
26+
lets: Let[] = [];
27+
intro?: Transition = null;
28+
outro?: Transition = null;
29+
animation?: Animation = null;
30+
children: INode[];
31+
scope: TemplateScope;
32+
33+
constructor(component: Component, parent, scope, info) {
34+
super(component, parent, scope, info);
35+
36+
this.name = info.name;
37+
38+
if (typeof info.tag === 'string') {
39+
this.tag = new Expression(component, this, scope, string_literal(info.tag));
40+
} else {
41+
this.tag = new Expression(component, this, scope, info.tag);
42+
}
43+
44+
info.attributes.forEach((node) => {
45+
switch (node.type) {
46+
case 'Action':
47+
this.actions.push(new Action(component, this, scope, node));
48+
break;
49+
50+
case 'Attribute':
51+
case 'Spread':
52+
this.attributes.push(new Attribute(component, this, scope, node));
53+
break;
54+
55+
case 'Binding':
56+
this.bindings.push(new Binding(component, this, scope, node));
57+
break;
58+
59+
case 'Class':
60+
this.classes.push(new Class(component, this, scope, node));
61+
break;
62+
63+
case 'EventHandler':
64+
this.handlers.push(new EventHandler(component, this, scope, node));
65+
break;
66+
67+
case 'Let': {
68+
const l = new Let(component, this, scope, node);
69+
this.lets.push(l);
70+
const dependencies = new Set([l.name.name]);
71+
72+
l.names.forEach((name) => {
73+
scope.add(name, dependencies, this);
74+
});
75+
break;
76+
}
77+
78+
case 'Transition': {
79+
const transition = new Transition(component, this, scope, node);
80+
if (node.intro) this.intro = transition;
81+
if (node.outro) this.outro = transition;
82+
break;
83+
}
84+
85+
case 'Animation':
86+
this.animation = new Animation(component, this, scope, node);
87+
break;
88+
89+
default:
90+
throw new Error(`Not implemented: ${node.type}`);
91+
}
92+
});
93+
94+
this.scope = scope;
95+
96+
this.children = map_children(component, this, this.scope, info.children);
97+
}
98+
}

src/compiler/compile/nodes/Element.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import Let from './Let';
1717
import TemplateScope from './shared/TemplateScope';
1818
import { INode } from './interfaces';
1919
import Component from '../Component';
20+
import Expression from './shared/Expression';
2021

2122
const svg = /^(?:altGlyph|altGlyphDef|altGlyphItem|animate|animateColor|animateMotion|animateTransform|circle|clipPath|color-profile|cursor|defs|desc|discard|ellipse|feBlend|feColorMatrix|feComponentTransfer|feComposite|feConvolveMatrix|feDiffuseLighting|feDisplacementMap|feDistantLight|feDropShadow|feFlood|feFuncA|feFuncB|feFuncG|feFuncR|feGaussianBlur|feImage|feMerge|feMergeNode|feMorphology|feOffset|fePointLight|feSpecularLighting|feSpotLight|feTile|feTurbulence|filter|font|font-face|font-face-format|font-face-name|font-face-src|font-face-uri|foreignObject|g|glyph|glyphRef|hatch|hatchpath|hkern|image|line|linearGradient|marker|mask|mesh|meshgradient|meshpatch|meshrow|metadata|missing-glyph|mpath|path|pattern|polygon|polyline|radialGradient|rect|set|solidcolor|stop|svg|switch|symbol|text|textPath|tref|tspan|unknown|use|view|vkern)$/;
2223

@@ -124,6 +125,7 @@ export default class Element extends Node {
124125
children: INode[];
125126
namespace: string;
126127
needs_manual_style_scoping: boolean;
128+
dynamic_tag?: Expression;
127129

128130
constructor(component: Component, parent, scope, info: any) {
129131
super(component, parent, scope, info);

src/compiler/compile/nodes/interfaces.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import ThenBlock from './ThenBlock';
3030
import Title from './Title';
3131
import Transition from './Transition';
3232
import Window from './Window';
33+
import DynamicElement from './DynamicElement';
3334

3435
// note: to write less types each of types in union below should have type defined as literal
3536
// https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions
@@ -43,6 +44,7 @@ export type INode = Action
4344
| Class
4445
| Comment
4546
| DebugTag
47+
| DynamicElement
4648
| EachBlock
4749
| Element
4850
| ElseBlock

src/compiler/compile/nodes/shared/map_children.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import AwaitBlock from '../AwaitBlock';
22
import Body from '../Body';
33
import Comment from '../Comment';
4+
import DynamicElement from '../DynamicElement';
45
import EachBlock from '../EachBlock';
56
import Element from '../Element';
67
import Head from '../Head';
@@ -24,6 +25,7 @@ function get_constructor(type) {
2425
case 'AwaitBlock': return AwaitBlock;
2526
case 'Body': return Body;
2627
case 'Comment': return Comment;
28+
case 'DynamicElement' : return DynamicElement;
2729
case 'EachBlock': return EachBlock;
2830
case 'Element': return Element;
2931
case 'Head': return Head;
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import Wrapper from './shared/Wrapper';
2+
import Renderer from '../Renderer';
3+
import Block from '../Block';
4+
import FragmentWrapper from './Fragment';
5+
import { b, x } from 'code-red';
6+
import { Identifier } from 'estree';
7+
import DynamicElement from '../../nodes/DynamicElement';
8+
import ElementWrapper from './Element/index';
9+
import create_debugging_comment from './shared/create_debugging_comment';
10+
import Element from '../../nodes/Element';
11+
12+
export default class DynamicElementWrapper extends Wrapper {
13+
fragment: FragmentWrapper;
14+
node: DynamicElement;
15+
elementWrapper: ElementWrapper;
16+
block: Block;
17+
dependencies: string[];
18+
var: Identifier = { type: 'Identifier', name: 'dynamic_element' };
19+
20+
constructor(
21+
renderer: Renderer,
22+
block: Block,
23+
parent: Wrapper,
24+
node: DynamicElement,
25+
strip_whitespace: boolean,
26+
next_sibling: Wrapper
27+
) {
28+
super(renderer, block, parent, node);
29+
30+
this.not_static_content();
31+
this.dependencies = node.tag.dynamic_dependencies();
32+
33+
if (this.dependencies.length) {
34+
block = block.child({
35+
comment: create_debugging_comment(node, renderer.component),
36+
name: renderer.component.get_unique_name('dynamic_element_block'),
37+
type: 'dynamic_element'
38+
});
39+
renderer.blocks.push(block);
40+
}
41+
42+
(node as unknown as Element).dynamic_tag = node.tag;
43+
44+
this.block = block;
45+
this.elementWrapper = new ElementWrapper(
46+
renderer,
47+
this.block,
48+
parent,
49+
(node as unknown) as Element,
50+
strip_whitespace,
51+
next_sibling
52+
);
53+
}
54+
55+
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
56+
if (this.dependencies.length === 0) {
57+
this.render_static_tag(block, parent_node, parent_nodes);
58+
} else {
59+
this.render_dynamic_tag(block, parent_node, parent_nodes);
60+
}
61+
}
62+
63+
render_static_tag(
64+
_block: Block,
65+
parent_node: Identifier,
66+
parent_nodes: Identifier
67+
) {
68+
this.elementWrapper.render(this.block, parent_node, parent_nodes);
69+
}
70+
71+
render_dynamic_tag(
72+
block: Block,
73+
parent_node: Identifier,
74+
parent_nodes: Identifier
75+
) {
76+
this.elementWrapper.render(
77+
this.block,
78+
null,
79+
(x`#nodes` as unknown) as Identifier
80+
);
81+
82+
const has_transitions = !!(
83+
this.block.has_intro_method || this.block.has_outro_method
84+
);
85+
const dynamic = this.block.has_update_method;
86+
87+
const previous_tag = block.get_unique_name('previous_tag');
88+
const snippet = this.node.tag.manipulate(block);
89+
block.add_variable(previous_tag, snippet);
90+
91+
const not_equal = this.renderer.component.component_options.immutable
92+
? x`@not_equal`
93+
: x`@safe_not_equal`;
94+
const condition = x`${this.renderer.dirty(
95+
this.dependencies
96+
)} && ${not_equal}(${previous_tag}, ${previous_tag} = ${snippet})`;
97+
98+
block.chunks.init.push(b`
99+
let ${this.var} = ${this.block.name}(#ctx);
100+
`);
101+
102+
block.chunks.create.push(b`${this.var}.c();`);
103+
104+
if (this.renderer.options.hydratable) {
105+
block.chunks.claim.push(b`${this.var}.l(${parent_nodes});`);
106+
}
107+
108+
block.chunks.mount.push(
109+
b`${this.var}.m(${parent_node || '#target'}, ${
110+
parent_node ? 'null' : '#anchor'
111+
});`
112+
);
113+
114+
const anchor = this.get_or_create_anchor(block, parent_node, parent_nodes);
115+
const body = b`
116+
${
117+
has_transitions
118+
? b`
119+
@group_outros();
120+
@transition_out(${this.var}, 1, 1, @noop);
121+
@check_outros();
122+
`
123+
: b`${this.var}.d(1);`
124+
}
125+
${this.var} = ${this.block.name}(#ctx);
126+
${this.var}.c();
127+
${has_transitions && b`@transition_in(${this.var})`}
128+
${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor});
129+
`;
130+
131+
if (dynamic) {
132+
block.chunks.update.push(b`
133+
if (${condition}) {
134+
${body}
135+
} else {
136+
${this.var}.p(#ctx, #dirty);
137+
}
138+
`);
139+
} else {
140+
block.chunks.update.push(b`
141+
if (${condition}) {
142+
${body}
143+
}
144+
`);
145+
}
146+
147+
if (has_transitions) {
148+
block.chunks.intro.push(b`@transition_in(${this.var})`);
149+
block.chunks.outro.push(b`@transition_out(${this.var})`);
150+
}
151+
152+
block.chunks.destroy.push(b`${this.var}.d(detaching)`);
153+
}
154+
}

src/compiler/compile/render_dom/wrappers/Element/index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,8 @@ export default class ElementWrapper extends Wrapper {
385385
return x`@element_is("${name}", ${is.render_chunks(block).reduce((lhs, rhs) => x`${lhs} + ${rhs}`)})`;
386386
}
387387

388-
return x`@element("${name}")`;
388+
const reference = this.node.dynamic_tag ? this.renderer.reference(this.node.dynamic_tag.node) : `"${name}"`;
389+
return x`@element(${reference})`;
389390
}
390391

391392
get_claim_statement(nodes: Identifier) {
@@ -399,7 +400,8 @@ export default class ElementWrapper extends Wrapper {
399400

400401
const svg = this.node.namespace === namespaces.svg ? 1 : null;
401402

402-
return x`@claim_element(${nodes}, "${name}", { ${attributes} }, ${svg})`;
403+
const reference = this.node.dynamic_tag ? this.renderer.reference(this.node.dynamic_tag.node) : `"${name}"`;
404+
return x`@claim_element(${nodes}, ${reference}, { ${attributes} }, ${svg})`;
403405
}
404406

405407
add_directives_in_order (block: Block) {

src/compiler/compile/render_dom/wrappers/Fragment.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Wrapper from './shared/Wrapper';
22
import AwaitBlock from './AwaitBlock';
33
import Body from './Body';
44
import DebugTag from './DebugTag';
5+
import DynamicElement from './DynamicElement';
56
import EachBlock from './EachBlock';
67
import Element from './Element/index';
78
import Head from './Head';
@@ -26,6 +27,7 @@ const wrappers = {
2627
Body,
2728
Comment: null,
2829
DebugTag,
30+
DynamicElement,
2931
EachBlock,
3032
Element,
3133
Head,

src/compiler/compile/render_ssr/Renderer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import AwaitBlock from './handlers/AwaitBlock';
22
import Comment from './handlers/Comment';
33
import DebugTag from './handlers/DebugTag';
4+
import DynamicElement from './handlers/DynamicElement';
45
import EachBlock from './handlers/EachBlock';
56
import Element from './handlers/Element';
67
import Head from './handlers/Head';
@@ -26,6 +27,7 @@ const handlers: Record<string, Handler> = {
2627
Body: noop,
2728
Comment,
2829
DebugTag,
30+
DynamicElement,
2931
EachBlock,
3032
Element,
3133
Head,

0 commit comments

Comments
 (0)