Skip to content

Commit

Permalink
Implement svelte:element for dynamically setting HTML DOM type
Browse files Browse the repository at this point in the history
  • Loading branch information
Alfred Ringstad committed Oct 1, 2020
1 parent 324f74b commit fe4158a
Show file tree
Hide file tree
Showing 30 changed files with 680 additions and 9 deletions.
98 changes: 98 additions & 0 deletions src/compiler/compile/nodes/DynamicElement.ts
@@ -0,0 +1,98 @@
import Node from './shared/Node';
import Attribute from './Attribute';
import Binding from './Binding';
import EventHandler from './EventHandler';
import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import Expression from './shared/Expression';
import Component from '../Component';
import map_children from './shared/map_children';
import Class from './Class';
import Transition from './Transition';
import Animation from './Animation';
import Action from './Action';
import { string_literal } from '../utils/stringify';

export default class DynamicElement extends Node {
type: 'DynamicElement';
name: string;
tag: Expression;
attributes: Attribute[] = [];
actions: Action[] = [];
bindings: Binding[] = [];
classes: Class[] = [];
handlers: EventHandler[] = [];
lets: Let[] = [];
intro?: Transition = null;
outro?: Transition = null;
animation?: Animation = null;
children: INode[];
scope: TemplateScope;

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

this.name = info.name;

if (typeof info.tag === 'string') {
this.tag = new Expression(component, this, scope, string_literal(info.tag));
} else {
this.tag = new Expression(component, this, scope, info.tag);
}

info.attributes.forEach((node) => {
switch (node.type) {
case 'Action':
this.actions.push(new Action(component, this, scope, node));
break;

case 'Attribute':
case 'Spread':
this.attributes.push(new Attribute(component, this, scope, node));
break;

case 'Binding':
this.bindings.push(new Binding(component, this, scope, node));
break;

case 'Class':
this.classes.push(new Class(component, this, scope, node));
break;

case 'EventHandler':
this.handlers.push(new EventHandler(component, this, scope, node));
break;

case 'Let': {
const l = new Let(component, this, scope, node);
this.lets.push(l);
const dependencies = new Set([l.name.name]);

l.names.forEach((name) => {
scope.add(name, dependencies, this);
});
break;
}

case 'Transition': {
const transition = new Transition(component, this, scope, node);
if (node.intro) this.intro = transition;
if (node.outro) this.outro = transition;
break;
}

case 'Animation':
this.animation = new Animation(component, this, scope, node);
break;

default:
throw new Error(`Not implemented: ${node.type}`);
}
});

this.scope = scope;

this.children = map_children(component, this, this.scope, info.children);
}
}
2 changes: 2 additions & 0 deletions src/compiler/compile/nodes/Element.ts
Expand Up @@ -17,6 +17,7 @@ import Let from './Let';
import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces';
import Component from '../Component';
import Expression from './shared/Expression';

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)$/;

Expand Down Expand Up @@ -124,6 +125,7 @@ export default class Element extends Node {
children: INode[];
namespace: string;
needs_manual_style_scoping: boolean;
dynamic_tag?: Expression;

constructor(component: Component, parent, scope, info: any) {
super(component, parent, scope, info);
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/compile/nodes/interfaces.ts
Expand Up @@ -30,6 +30,7 @@ import ThenBlock from './ThenBlock';
import Title from './Title';
import Transition from './Transition';
import Window from './Window';
import DynamicElement from './DynamicElement';

// note: to write less types each of types in union below should have type defined as literal
// https://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions
Expand All @@ -43,6 +44,7 @@ export type INode = Action
| Class
| Comment
| DebugTag
| DynamicElement
| EachBlock
| Element
| ElseBlock
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/compile/nodes/shared/map_children.ts
@@ -1,6 +1,7 @@
import AwaitBlock from '../AwaitBlock';
import Body from '../Body';
import Comment from '../Comment';
import DynamicElement from '../DynamicElement';
import EachBlock from '../EachBlock';
import Element from '../Element';
import Head from '../Head';
Expand All @@ -24,6 +25,7 @@ function get_constructor(type) {
case 'AwaitBlock': return AwaitBlock;
case 'Body': return Body;
case 'Comment': return Comment;
case 'DynamicElement' : return DynamicElement;
case 'EachBlock': return EachBlock;
case 'Element': return Element;
case 'Head': return Head;
Expand Down
154 changes: 154 additions & 0 deletions src/compiler/compile/render_dom/wrappers/DynamicElement.ts
@@ -0,0 +1,154 @@
import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer';
import Block from '../Block';
import FragmentWrapper from './Fragment';
import { b, x } from 'code-red';
import { Identifier } from 'estree';
import DynamicElement from '../../nodes/DynamicElement';
import ElementWrapper from './Element/index';
import create_debugging_comment from './shared/create_debugging_comment';
import Element from '../../nodes/Element';

export default class DynamicElementWrapper extends Wrapper {
fragment: FragmentWrapper;
node: DynamicElement;
elementWrapper: ElementWrapper;
block: Block;
dependencies: string[];
var: Identifier = { type: 'Identifier', name: 'dynamic_element' };

constructor(
renderer: Renderer,
block: Block,
parent: Wrapper,
node: DynamicElement,
strip_whitespace: boolean,
next_sibling: Wrapper
) {
super(renderer, block, parent, node);

this.not_static_content();
this.dependencies = node.tag.dynamic_dependencies();

if (this.dependencies.length) {
block = block.child({
comment: create_debugging_comment(node, renderer.component),
name: renderer.component.get_unique_name('dynamic_element_block'),
type: 'dynamic_element'
});
renderer.blocks.push(block);
}

(node as unknown as Element).dynamic_tag = node.tag;

this.block = block;
this.elementWrapper = new ElementWrapper(
renderer,
this.block,
parent,
(node as unknown) as Element,
strip_whitespace,
next_sibling
);
}

render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
if (this.dependencies.length === 0) {
this.render_static_tag(block, parent_node, parent_nodes);
} else {
this.render_dynamic_tag(block, parent_node, parent_nodes);
}
}

render_static_tag(
_block: Block,
parent_node: Identifier,
parent_nodes: Identifier
) {
this.elementWrapper.render(this.block, parent_node, parent_nodes);
}

render_dynamic_tag(
block: Block,
parent_node: Identifier,
parent_nodes: Identifier
) {
this.elementWrapper.render(
this.block,
null,
(x`#nodes` as unknown) as Identifier
);

const has_transitions = !!(
this.block.has_intro_method || this.block.has_outro_method
);
const dynamic = this.block.has_update_method;

const previous_tag = block.get_unique_name('previous_tag');
const snippet = this.node.tag.manipulate(block);
block.add_variable(previous_tag, snippet);

const not_equal = this.renderer.component.component_options.immutable
? x`@not_equal`
: x`@safe_not_equal`;
const condition = x`${this.renderer.dirty(
this.dependencies
)} && ${not_equal}(${previous_tag}, ${previous_tag} = ${snippet})`;

block.chunks.init.push(b`
let ${this.var} = ${this.block.name}(#ctx);
`);

block.chunks.create.push(b`${this.var}.c();`);

if (this.renderer.options.hydratable) {
block.chunks.claim.push(b`${this.var}.l(${parent_nodes});`);
}

block.chunks.mount.push(
b`${this.var}.m(${parent_node || '#target'}, ${
parent_node ? 'null' : '#anchor'
});`
);

const anchor = this.get_or_create_anchor(block, parent_node, parent_nodes);
const body = b`
${
has_transitions
? b`
@group_outros();
@transition_out(${this.var}, 1, 1, @noop);
@check_outros();
`
: b`${this.var}.d(1);`
}
${this.var} = ${this.block.name}(#ctx);
${this.var}.c();
${has_transitions && b`@transition_in(${this.var})`}
${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor});
`;

if (dynamic) {
block.chunks.update.push(b`
if (${condition}) {
${body}
} else {
${this.var}.p(#ctx, #dirty);
}
`);
} else {
block.chunks.update.push(b`
if (${condition}) {
${body}
}
`);
}

if (has_transitions) {
block.chunks.intro.push(b`@transition_in(${this.var})`);
block.chunks.outro.push(b`@transition_out(${this.var})`);
}

block.chunks.destroy.push(b`${this.var}.d(detaching)`);
}
}
6 changes: 4 additions & 2 deletions src/compiler/compile/render_dom/wrappers/Element/index.ts
Expand Up @@ -385,7 +385,8 @@ export default class ElementWrapper extends Wrapper {
return x`@element_is("${name}", ${is.render_chunks(block).reduce((lhs, rhs) => x`${lhs} + ${rhs}`)})`;
}

return x`@element("${name}")`;
const reference = this.node.dynamic_tag ? this.renderer.reference(this.node.dynamic_tag.node) : `"${name}"`;
return x`@element(${reference})`;
}

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

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

return x`@claim_element(${nodes}, "${name}", { ${attributes} }, ${svg})`;
const reference = this.node.dynamic_tag ? this.renderer.reference(this.node.dynamic_tag.node) : `"${name}"`;
return x`@claim_element(${nodes}, ${reference}, { ${attributes} }, ${svg})`;
}

add_directives_in_order (block: Block) {
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/compile/render_dom/wrappers/Fragment.ts
Expand Up @@ -2,6 +2,7 @@ import Wrapper from './shared/Wrapper';
import AwaitBlock from './AwaitBlock';
import Body from './Body';
import DebugTag from './DebugTag';
import DynamicElement from './DynamicElement';
import EachBlock from './EachBlock';
import Element from './Element/index';
import Head from './Head';
Expand All @@ -26,6 +27,7 @@ const wrappers = {
Body,
Comment: null,
DebugTag,
DynamicElement,
EachBlock,
Element,
Head,
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/compile/render_ssr/Renderer.ts
@@ -1,6 +1,7 @@
import AwaitBlock from './handlers/AwaitBlock';
import Comment from './handlers/Comment';
import DebugTag from './handlers/DebugTag';
import DynamicElement from './handlers/DynamicElement';
import EachBlock from './handlers/EachBlock';
import Element from './handlers/Element';
import Head from './handlers/Head';
Expand All @@ -26,6 +27,7 @@ const handlers: Record<string, Handler> = {
Body: noop,
Comment,
DebugTag,
DynamicElement,
EachBlock,
Element,
Head,
Expand Down

0 comments on commit fe4158a

Please sign in to comment.