Implement svelte:element for dynamically setting HTML DOM type

pull/5481/head
Alfred Ringstad 6 years ago
parent 26d006591d
commit f001107711

@ -5,13 +5,14 @@ import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces'; import { TemplateNode } from '../../interfaces';
import Element from './Element'; import Element from './Element';
import EachBlock from './EachBlock'; import EachBlock from './EachBlock';
import DynamicElement from './DynamicElement';
export default class Animation extends Node { export default class Animation extends Node {
type: 'Animation'; type: 'Animation';
name: string; name: string;
expression: Expression; expression: Expression;
constructor(component: Component, parent: Element, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Element | DynamicElement, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
component.warn_if_undefined(info.name, info, scope); component.warn_if_undefined(info.name, info, scope);

@ -10,6 +10,7 @@ import Element from './Element';
import InlineComponent from './InlineComponent'; import InlineComponent from './InlineComponent';
import Window from './Window'; import Window from './Window';
import { clone } from '../../utils/clone'; import { clone } from '../../utils/clone';
import DynamicElement from './DynamicElement';
// TODO this should live in a specific binding // TODO this should live in a specific binding
const read_only_media_attributes = new Set([ const read_only_media_attributes = new Set([
@ -31,7 +32,7 @@ export default class Binding extends Node {
is_contextual: boolean; is_contextual: boolean;
is_readonly: boolean; is_readonly: boolean;
constructor(component: Component, parent: Element | InlineComponent | Window, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Element | InlineComponent | Window | DynamicElement, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') { if (info.expression.type !== 'Identifier' && info.expression.type !== 'MemberExpression') {

@ -0,0 +1,99 @@
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';
import { Literal } from 'estree';
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) as Literal);
} 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);
}
}

@ -17,6 +17,7 @@ import Let from './Let';
import TemplateScope from './shared/TemplateScope'; import TemplateScope from './shared/TemplateScope';
import { INode } from './interfaces'; import { INode } from './interfaces';
import Component from '../Component'; 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)$/; 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)$/;
@ -129,6 +130,7 @@ export default class Element extends Node {
children: INode[]; children: INode[];
namespace: string; namespace: string;
needs_manual_style_scoping: boolean; needs_manual_style_scoping: boolean;
dynamic_tag?: Expression;
constructor(component: Component, parent: Node, scope: TemplateScope, info: any) { constructor(component: Component, parent: Node, scope: TemplateScope, info: any) {
super(component, parent, scope, info); super(component, parent, scope, info);

@ -4,6 +4,7 @@ import Component from '../Component';
import TemplateScope from './shared/TemplateScope'; import TemplateScope from './shared/TemplateScope';
import { TemplateNode } from '../../interfaces'; import { TemplateNode } from '../../interfaces';
import Element from './Element'; import Element from './Element';
import DynamicElement from './DynamicElement';
export default class Transition extends Node { export default class Transition extends Node {
type: 'Transition'; type: 'Transition';
@ -12,7 +13,7 @@ export default class Transition extends Node {
expression: Expression; expression: Expression;
is_local: boolean; is_local: boolean;
constructor(component: Component, parent: Element, scope: TemplateScope, info: TemplateNode) { constructor(component: Component, parent: Element | DynamicElement, scope: TemplateScope, info: TemplateNode) {
super(component, parent, scope, info); super(component, parent, scope, info);
component.warn_if_undefined(info.name, info, scope); component.warn_if_undefined(info.name, info, scope);

@ -32,6 +32,7 @@ import ThenBlock from './ThenBlock';
import Title from './Title'; import Title from './Title';
import Transition from './Transition'; import Transition from './Transition';
import Window from './Window'; 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 // note: to write less types each of types in union below should have type defined as literal
// https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#discriminating-unions // https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#discriminating-unions
@ -45,6 +46,7 @@ export type INode = Action
| Class | Class
| Comment | Comment
| DebugTag | DebugTag
| DynamicElement
| EachBlock | EachBlock
| Element | Element
| ElseBlock | ElseBlock

@ -1,6 +1,7 @@
import AwaitBlock from '../AwaitBlock'; import AwaitBlock from '../AwaitBlock';
import Body from '../Body'; import Body from '../Body';
import Comment from '../Comment'; import Comment from '../Comment';
import DynamicElement from '../DynamicElement';
import EachBlock from '../EachBlock'; import EachBlock from '../EachBlock';
import Element from '../Element'; import Element from '../Element';
import Head from '../Head'; import Head from '../Head';
@ -25,6 +26,7 @@ function get_constructor(type) {
case 'AwaitBlock': return AwaitBlock; case 'AwaitBlock': return AwaitBlock;
case 'Body': return Body; case 'Body': return Body;
case 'Comment': return Comment; case 'Comment': return Comment;
case 'DynamicElement' : return DynamicElement;
case 'EachBlock': return EachBlock; case 'EachBlock': return EachBlock;
case 'Element': return Element; case 'Element': return Element;
case 'Head': return Head; case 'Head': return Head;

@ -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)`);
}
}

@ -247,7 +247,7 @@ export default class ElementWrapper extends Wrapper {
if (renderer.options.hydratable) { if (renderer.options.hydratable) {
if (parent_nodes) { if (parent_nodes) {
block.chunks.claim.push(b` block.chunks.claim.push(b`
${node} = ${this.get_claim_statement(parent_nodes)}; ${node} = ${this.get_claim_statement(block, parent_nodes)};
`); `);
if (!this.void && this.node.children.length > 0) { if (!this.void && this.node.children.length > 0) {
@ -368,10 +368,11 @@ export default class ElementWrapper extends Wrapper {
return x`@element_is("${name}", ${is.render_chunks(block).reduce((lhs, rhs) => x`${lhs} + ${rhs}`)})`; 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.node.dynamic_tag.manipulate(block) : `"${name}"`;
return x`@element(${reference})`;
} }
get_claim_statement(nodes: Identifier) { get_claim_statement(block: Block, nodes: Identifier) {
const attributes = this.node.attributes const attributes = this.node.attributes
.filter((attr) => attr.type === 'Attribute') .filter((attr) => attr.type === 'Attribute')
.map((attr) => p`${attr.name}: true`); .map((attr) => p`${attr.name}: true`);
@ -382,7 +383,8 @@ export default class ElementWrapper extends Wrapper {
const svg = this.node.namespace === namespaces.svg ? 1 : null; const svg = this.node.namespace === namespaces.svg ? 1 : null;
return x`@claim_element(${nodes}, "${name}", { ${attributes} }, ${svg})`; const reference = this.node.dynamic_tag ? this.node.dynamic_tag.manipulate(block) : `"${name}"`;
return x`@claim_element(${nodes}, ${reference}, { ${attributes} }, ${svg})`;
} }
add_directives_in_order (block: Block) { add_directives_in_order (block: Block) {

@ -2,6 +2,7 @@ import Wrapper from './shared/Wrapper';
import AwaitBlock from './AwaitBlock'; import AwaitBlock from './AwaitBlock';
import Body from './Body'; import Body from './Body';
import DebugTag from './DebugTag'; import DebugTag from './DebugTag';
import DynamicElement from './DynamicElement';
import EachBlock from './EachBlock'; import EachBlock from './EachBlock';
import Element from './Element/index'; import Element from './Element/index';
import Head from './Head'; import Head from './Head';
@ -27,6 +28,7 @@ const wrappers = {
Body, Body,
Comment: null, Comment: null,
DebugTag, DebugTag,
DynamicElement,
EachBlock, EachBlock,
Element, Element,
Head, Head,

@ -1,6 +1,7 @@
import AwaitBlock from './handlers/AwaitBlock'; import AwaitBlock from './handlers/AwaitBlock';
import Comment from './handlers/Comment'; import Comment from './handlers/Comment';
import DebugTag from './handlers/DebugTag'; import DebugTag from './handlers/DebugTag';
import DynamicElement from './handlers/DynamicElement';
import EachBlock from './handlers/EachBlock'; import EachBlock from './handlers/EachBlock';
import Element from './handlers/Element'; import Element from './handlers/Element';
import Head from './handlers/Head'; import Head from './handlers/Head';
@ -27,6 +28,7 @@ const handlers: Record<string, Handler> = {
Body: noop, Body: noop,
Comment, Comment,
DebugTag, DebugTag,
DynamicElement,
EachBlock, EachBlock,
Element, Element,
Head, Head,

@ -0,0 +1,215 @@
import {
get_attribute_value,
get_class_attribute_value
} from './shared/get_attribute_value';
import { get_slot_scope } from './shared/get_slot_scope';
import { boolean_attributes } from './shared/boolean_attributes';
import Renderer, { RenderOptions } from '../Renderer';
import DynamicElement from '../../nodes/DynamicElement';
import ElementHandler from './Element';
import { x } from 'code-red';
import Expression from '../../nodes/shared/Expression';
import remove_whitespace_children from './utils/remove_whitespace_children';
import Element from '../../nodes/Element';
import { Expression as ESExpression } from 'estree';
export default function (
node: DynamicElement,
renderer: Renderer,
options: RenderOptions & {
slot_scopes: Map<any, any>;
}
) {
const dependencies = node.tag.dynamic_dependencies();
if (dependencies.length === 0) {
((node as unknown) as Element).dynamic_tag = node.tag;
ElementHandler((node as unknown) as Element, renderer, options);
} else {
const children = remove_whitespace_children(node.children, node.next);
// awkward special case
let node_contents;
const contenteditable = node.attributes.some(
(attribute) => attribute.name === 'contenteditable'
);
const slot = node.get_static_attribute_value('slot');
const nearest_inline_component = node.find_nearest(/InlineComponent/);
if (slot && nearest_inline_component) {
renderer.push();
}
renderer.add_string('<');
renderer.add_expression(node.tag.node as ESExpression);
const class_expression_list = node.classes.map((class_directive) => {
const { expression, name } = class_directive;
const snippet = expression ? expression.node : x`#ctx.${name}`; // TODO is this right?
return x`${snippet} ? "${name}" : ""`;
});
// if (node.needs_manual_style_scoping) {
// class_expression_list.push(x`"${node.component.stylesheet.id}"`);
// }
const class_expression =
class_expression_list.length > 0 &&
class_expression_list.reduce((lhs, rhs) => x`${lhs} + ' ' + ${rhs}`);
if (node.attributes.some((attr) => attr.is_spread)) {
// TODO dry this out
const args = [];
node.attributes.forEach((attribute) => {
if (attribute.is_spread) {
args.push(attribute.expression.node);
} else {
const name = attribute.name.toLowerCase();
if (attribute.is_true) {
args.push(x`{ ${attribute.name}: true }`);
} else if (
boolean_attributes.has(name) &&
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
args.push(
x`{ ${attribute.name}: ${
(attribute.chunks[0] as Expression).node
} || null }`
);
} else {
args.push(
x`{ ${attribute.name}: ${get_attribute_value(attribute)} }`
);
}
}
});
renderer.add_expression(x`@spread([${args}], ${class_expression})`);
} else {
let add_class_attribute = !!class_expression;
node.attributes.forEach((attribute) => {
const name = attribute.name.toLowerCase();
if (attribute.is_true) {
renderer.add_string(` ${attribute.name}`);
} else if (
boolean_attributes.has(name) &&
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
// a boolean attribute with one non-Text chunk
renderer.add_string(' ');
renderer.add_expression(
x`${(attribute.chunks[0] as Expression).node} ? "${
attribute.name
}" : ""`
);
} else if (name === 'class' && class_expression) {
add_class_attribute = false;
renderer.add_string(` ${attribute.name}="`);
renderer.add_expression(
x`[${get_class_attribute_value(
attribute
)}, ${class_expression}].join(' ').trim()`
);
renderer.add_string('"');
} else if (
attribute.chunks.length === 1 &&
attribute.chunks[0].type !== 'Text'
) {
const snippet = (attribute.chunks[0] as Expression).node;
renderer.add_expression(
x`@add_attribute("${attribute.name}", ${snippet}, ${
boolean_attributes.has(name) ? 1 : 0
})`
);
} else {
renderer.add_string(` ${attribute.name}="`);
renderer.add_expression(
(name === 'class'
? get_class_attribute_value
: get_attribute_value)(attribute)
);
renderer.add_string('"');
}
});
if (add_class_attribute) {
renderer.add_expression(
x`@add_classes([${class_expression}].join(' ').trim())`
);
}
}
node.bindings.forEach((binding) => {
const { name, expression } = binding;
if (binding.is_readonly) {
return;
}
if (name === 'group') {
// TODO server-render group bindings
} else if (
contenteditable &&
(name === 'textContent' || name === 'innerHTML')
) {
node_contents = expression.node;
// TODO where was this used?
// value = name === 'textContent' ? x`@escape($$value)` : x`$$value`;
} else {
const snippet = expression.node;
renderer.add_expression(x`@add_attribute("${name}", ${snippet}, 1)`);
}
});
if (options.hydratable && options.head_id) {
renderer.add_string(` data-svelte="${options.head_id}"`);
}
renderer.add_string('>');
if (node_contents !== undefined) {
if (contenteditable) {
renderer.push();
renderer.render(children, options);
const result = renderer.pop();
renderer.add_expression(
x`($$value => $$value === void 0 ? ${result} : $$value)(${node_contents})`
);
} else {
renderer.add_expression(node_contents);
}
renderer.add_string('</');
renderer.add_expression(node.tag.node as ESExpression);
renderer.add_string('>');
} else if (slot && nearest_inline_component) {
renderer.render(children, options);
renderer.add_string('</');
renderer.add_expression(node.tag.node as ESExpression);
renderer.add_string('>');
const lets = node.lets;
const seen = new Set(lets.map((l) => l.name.name));
nearest_inline_component.lets.forEach((l) => {
if (!seen.has(l.name.name)) lets.push(l);
});
options.slot_scopes.set(slot, {
input: get_slot_scope(node.lets),
output: renderer.pop()
});
} else {
renderer.render(children, options);
renderer.add_string('</');
renderer.add_expression(node.tag.node as ESExpression);
renderer.add_string('>');
}
}
}

@ -6,6 +6,7 @@ import Element from '../../nodes/Element';
import { x } from 'code-red'; import { x } from 'code-red';
import Expression from '../../nodes/shared/Expression'; import Expression from '../../nodes/shared/Expression';
import remove_whitespace_children from './utils/remove_whitespace_children'; import remove_whitespace_children from './utils/remove_whitespace_children';
import { Expression as ESExpression } from 'estree';
export default function(node: Element, renderer: Renderer, options: RenderOptions) { export default function(node: Element, renderer: Renderer, options: RenderOptions) {
@ -20,7 +21,12 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
node.attributes.some((attribute) => attribute.name === 'contenteditable') node.attributes.some((attribute) => attribute.name === 'contenteditable')
); );
renderer.add_string(`<${node.name}`); if (node.dynamic_tag) {
renderer.add_string('<');
renderer.add_expression(node.dynamic_tag.node as ESExpression);
} else {
renderer.add_string(`<${node.name}`);
}
const class_expression_list = node.classes.map(class_directive => { const class_expression_list = node.classes.map(class_directive => {
const { expression, name } = class_directive; const { expression, name } = class_directive;
@ -136,13 +142,25 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
} }
if (!is_void(node.name)) { if (!is_void(node.name)) {
renderer.add_string(`</${node.name}>`); if (node.dynamic_tag) {
renderer.add_string('</');
renderer.add_expression(node.dynamic_tag.node as ESExpression);
renderer.add_string('>');
} else {
renderer.add_string(`</${node.name}>`);
}
} }
} else { } else {
renderer.render(children, options); renderer.render(children, options);
if (!is_void(node.name)) { if (!is_void(node.name)) {
renderer.add_string(`</${node.name}>`); if (node.dynamic_tag) {
renderer.add_string('</');
renderer.add_expression(node.dynamic_tag.node as ESExpression);
renderer.add_string('>');
} else {
renderer.add_string(`</${node.name}>`);
}
} }
} }
} }

@ -18,7 +18,7 @@ const meta_tags = new Map([
['svelte:body', 'Body'] ['svelte:body', 'Body']
]); ]);
const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component', 'svelte:fragment'); const valid_meta_tags = Array.from(meta_tags.keys()).concat('svelte:self', 'svelte:component', 'svelte:fragment', 'svelte:element');
const specials = new Map([ const specials = new Map([
[ [
@ -40,6 +40,7 @@ const specials = new Map([
const SELF = /^svelte:self(?=[\s/>])/; const SELF = /^svelte:self(?=[\s/>])/;
const COMPONENT = /^svelte:component(?=[\s/>])/; const COMPONENT = /^svelte:component(?=[\s/>])/;
const SLOT = /^svelte:fragment(?=[\s/>])/; const SLOT = /^svelte:fragment(?=[\s/>])/;
const ELEMENT = /^svelte:element(?=[\s/>])/;
function parent_is_head(stack) { function parent_is_head(stack) {
let i = stack.length; let i = stack.length;
@ -109,8 +110,9 @@ export default function tag(parser: Parser) {
? meta_tags.get(name) ? meta_tags.get(name)
: (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent' : (/[A-Z]/.test(name[0]) || name === 'svelte:self' || name === 'svelte:component') ? 'InlineComponent'
: name === 'svelte:fragment' ? 'SlotTemplate' : name === 'svelte:fragment' ? 'SlotTemplate'
: name === 'title' && parent_is_head(parser.stack) ? 'Title' : (name === 'svelte:element') ? 'DynamicElement'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element'; : name === 'title' && parent_is_head(parser.stack) ? 'Title'
: name === 'slot' && !parser.customElement ? 'Slot' : 'Element';
const element: TemplateNode = { const element: TemplateNode = {
start, start,
@ -197,6 +199,26 @@ export default function tag(parser: Parser) {
element.expression = definition.value[0].expression; element.expression = definition.value[0].expression;
} }
if (name === 'svelte:element') {
const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'tag');
if (!~index) {
parser.error({
code: 'missing-element-definition',
message: '<svelte:element> must have a \'tag\' attribute'
}, start);
}
const definition = element.attributes.splice(index, 1)[0];
if (definition.value === true || definition.value.length !== 1 || (definition.value[0].type !== 'Text' && definition.value[0].type !== 'MustacheTag' && definition.value[0].type !== 'AttributeShorthand')) {
parser.error({
code: 'invalid-element-definition',
message: 'invalid element definition'
}, definition.start);
}
element.tag = definition.value[0].data || definition.value[0].expression;
}
// special cases top-level <script> and <style> // special cases top-level <script> and <style>
if (specials.has(name) && parser.stack.length === 1) { if (specials.has(name) && parser.stack.length === 1) {
const special = specials.get(name); const special = specials.get(name);
@ -266,6 +288,7 @@ function read_tag_name(parser: Parser) {
} }
if (parser.read(COMPONENT)) return 'svelte:component'; if (parser.read(COMPONENT)) return 'svelte:component';
if (parser.read(ELEMENT)) return 'svelte:element';
if (parser.read(SLOT)) return 'svelte:fragment'; if (parser.read(SLOT)) return 'svelte:fragment';

@ -0,0 +1 @@
<svelte:element tag="div"></svelte:element>

@ -0,0 +1,18 @@
{
"html": {
"start": 0,
"end": 43,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 43,
"type": "DynamicElement",
"name": "svelte:element",
"attributes": [],
"children": [],
"tag": "div"
}
]
}
}

@ -0,0 +1 @@
<svelte:element tag={tag}></svelte:element>

@ -0,0 +1,33 @@
{
"html": {
"start": 0,
"end": 43,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 43,
"type": "DynamicElement",
"name": "svelte:element",
"attributes": [],
"children": [],
"tag": {
"start": 21,
"end": 24,
"loc": {
"start": {
"line": 1,
"column": 21
},
"end": {
"line": 1,
"column": 24
}
},
"name": "tag",
"type": "Identifier"
}
}
]
}
}

@ -1,6 +1,6 @@
{ {
"code": "invalid-tag-name", "code": "invalid-tag-name",
"message": "Valid <svelte:...> tag names are svelte:head, svelte:options, svelte:window, svelte:body, svelte:self, svelte:component or svelte:fragment", "message": "Valid <svelte:...> tag names are svelte:head, svelte:options, svelte:window, svelte:body, svelte:self, svelte:component, svelte:fragment or svelte:element",
"pos": 10, "pos": 10,
"start": { "start": {
"character": 10, "character": 10,

@ -0,0 +1,22 @@
export default {
props: {
tag: 'div',
text: 'Foo'
},
html: '<div><div>Foo</div></div>',
test({ assert, component, target }) {
const innerDiv = target.querySelector('div > div');
component.tag = 'h1';
component.text = 'Bar';
assert.htmlEqual(
target.innerHTML,
`
<h1><div>Bar</div></h1>
`
);
assert.equal(innerDiv, target.querySelector('h1 > div'));
}
};

@ -0,0 +1,9 @@
<script>
export let tag = 'div', text = 'Foo';
</script>
<svelte:element tag={tag}>
<div>
{text}
</div>
</svelte:element>

@ -0,0 +1,17 @@
export default {
props: {
tag: 'div'
},
html: '<div>Foo</div>',
test({ assert, component, target }) {
component.tag = 'h1';
assert.htmlEqual(
target.innerHTML,
`
<h1>Foo</h1>
`
);
}
};

@ -0,0 +1,5 @@
<script>
export let tag = 'div';
</script>
<svelte:element tag={tag}>Foo</svelte:element>

@ -0,0 +1,3 @@
export default {
html: '<div>Foo</div>'
};

@ -0,0 +1 @@
<svelte:element tag={"div"}>Foo</svelte:element>

@ -0,0 +1,16 @@
let clicked = false;
export default {
props: {
tag: 'div',
onClick: () => clicked = true
},
html: '<div style="display: inline;">Foo</div>',
async test({ assert, target, window }) {
const div = target.querySelector('div');
await div.dispatchEvent(new window.MouseEvent('click'));
assert.equal(clicked, true);
}
};

@ -0,0 +1,6 @@
<script>
const tag = "div";
export let onClick;
</script>
<svelte:element tag={tag} style="display: inline;" on:click={onClick}>Foo</svelte:element>

@ -0,0 +1,3 @@
export default {
html: '<div>Foo</div>'
};

@ -0,0 +1 @@
<svelte:element tag="div">Foo</svelte:element>

@ -0,0 +1,20 @@
export default {
props: {
tag: 'di',
text: 'Foo'
},
html: '<div>Foo</div>',
test({ assert, component, target }) {
const div = target.firstChild;
component.tag = 'na';
component.text = 'Bar';
assert.htmlEqual(target.innerHTML, `
<nav>Bar</nav>
`);
const h1 = target.firstChild;
assert.notEqual(div, h1);
}
};

@ -0,0 +1,6 @@
<script>
export let tag = "di";
export let text = "Foo";
</script>
<svelte:element tag={tag + "v"}>{text}</svelte:element>

@ -0,0 +1 @@
<svelte:element tag="div">Foo</svelte:element>

@ -0,0 +1,7 @@
<script>
let heading = 'h1';
let tag = 'div';
</script>
<svelte:element tag={heading}>Foo</svelte:element>
<svelte:element {tag}>Bar</svelte:element>

@ -0,0 +1,47 @@
/* test/sourcemaps/samples/two-scripts/input.svelte generated by Svelte vx.xx.x */
import {
SvelteComponent,
detach,
init,
insert,
noop,
safe_not_equal,
text
} from "svelte/internal";
function create_fragment(ctx) {
let t_value = foo.bar.baz + "";
let t;
return {
c() {
t = text(t_value);
},
m(target, anchor) {
insert(target, t, anchor);
},
p: noop,
i: noop,
o: noop,
d(detaching) {
if (detaching) detach(t);
}
};
}
let first;
function assertThisLine() {
}
class Input extends SvelteComponent {
constructor(options) {
super();
init(this, options, null, create_fragment, safe_not_equal, {});
}
}
export default Input;
export { first };
//# sourceMappingURL=output.js.map
Loading…
Cancel
Save