Reuse DOM elements if possible. Add dev warnings when passing nullish values as tag. Throw an error when trying to bind other than this. Add more tests.

pull/5481/head
Alfred Ringstad 5 years ago
parent 4fcc68188d
commit 54e1c23b32

@ -97,6 +97,19 @@ export default class DynamicElement extends Node {
this.scope = scope; this.scope = scope;
this.children = map_children(component, this, this.scope, info.children); this.children = map_children(component, this, this.scope, info.children);
this.validate();
}
validate() {
this.bindings.forEach(binding => {
if (binding.name !== 'this') {
this.component.error(binding, {
code: 'invalid-binding',
message: `'${binding.name}' is not a valid binding. svelte:element only supports bind:this`
});
}
});
} }
add_css_class() { add_css_class() {

@ -1,7 +1,6 @@
import Wrapper from './shared/Wrapper'; import Wrapper from './shared/Wrapper';
import Renderer from '../Renderer'; import Renderer from '../Renderer';
import Block from '../Block'; import Block from '../Block';
import FragmentWrapper from './Fragment';
import { b, x } from 'code-red'; import { b, x } from 'code-red';
import { Identifier } from 'estree'; import { Identifier } from 'estree';
import DynamicElement from '../../nodes/DynamicElement'; import DynamicElement from '../../nodes/DynamicElement';
@ -10,7 +9,6 @@ import create_debugging_comment from './shared/create_debugging_comment';
import Element from '../../nodes/Element'; import Element from '../../nodes/Element';
export default class DynamicElementWrapper extends Wrapper { export default class DynamicElementWrapper extends Wrapper {
fragment: FragmentWrapper;
node: DynamicElement; node: DynamicElement;
elementWrapper: ElementWrapper; elementWrapper: ElementWrapper;
block: Block; block: Block;
@ -112,43 +110,45 @@ export default class DynamicElementWrapper extends Wrapper {
); );
const anchor = this.get_or_create_anchor(block, parent_node, parent_nodes); 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) { if (has_transitions) {
block.chunks.update.push(b` block.chunks.intro.push(b`@transition_in(${this.var})`);
if (${condition}) { block.chunks.outro.push(b`@transition_out(${this.var})`);
${body}
const body = b`
@group_outros();
@transition_out(${this.var}, 1, 1, @noop);
@check_outros();
${this.var} = ${this.block.name}(#ctx);
${this.var}.c();
@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 { } else {
${this.var}.p(#ctx, #dirty); block.chunks.update.push(b`
if (${condition}) {
${body}
}
`);
} }
`); } else if (dynamic) {
} else {
block.chunks.update.push(b` block.chunks.update.push(b`
${this.var}.p(#ctx, #dirty);
if (${condition}) { if (${condition}) {
${body} ${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor});
} }
`); `);
} }
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)`); block.chunks.destroy.push(b`${this.var}.d(detaching)`);
} }
} }

@ -211,6 +211,10 @@ export default class ElementWrapper extends Wrapper {
} }
}); });
if (node.dynamic_tag) {
block.add_dependencies(node.dynamic_tag.dependencies);
}
if (this.parent) { if (this.parent) {
if (node.actions.length > 0 || if (node.actions.length > 0 ||
node.animation || node.animation ||
@ -244,6 +248,14 @@ export default class ElementWrapper extends Wrapper {
b`${node} = ${render_statement};` b`${node} = ${render_statement};`
); );
if (this.node.dynamic_tag && this.renderer.options.dev) {
block.chunks.create.push(b`@validate_dynamic_element(${this.node.dynamic_tag.manipulate(block)});`);
if (renderer.options.hydratable) {
block.chunks.claim.push(b`@validate_dynamic_element(${this.node.dynamic_tag.manipulate(block)});`);
}
}
if (renderer.options.hydratable) { if (renderer.options.hydratable) {
if (parent_nodes) { if (parent_nodes) {
block.chunks.claim.push(b` block.chunks.claim.push(b`
@ -278,13 +290,14 @@ export default class ElementWrapper extends Wrapper {
block.chunks.destroy.push(b`if (detaching) @detach(${node});`); block.chunks.destroy.push(b`if (detaching) @detach(${node});`);
} }
let staticChildren = null;
// insert static children with textContent or innerHTML // insert static children with textContent or innerHTML
const can_use_textcontent = this.can_use_textcontent(); const can_use_textcontent = this.can_use_textcontent();
if (!this.node.namespace && (this.can_use_innerhtml || can_use_textcontent) && this.fragment.nodes.length > 0) { if (!this.node.namespace && (this.can_use_innerhtml || can_use_textcontent) && this.fragment.nodes.length > 0) {
if (this.fragment.nodes.length === 1 && this.fragment.nodes[0].node.type === 'Text') { if (this.fragment.nodes.length === 1 && this.fragment.nodes[0].node.type === 'Text') {
block.chunks.create.push( staticChildren = b`${node}.textContent = ${string_literal((this.fragment.nodes[0] as TextWrapper).data)};`;
b`${node}.textContent = ${string_literal((this.fragment.nodes[0] as TextWrapper).data)};` block.chunks.create.push(staticChildren);
);
} else { } else {
const state = { const state = {
quasi: { quasi: {
@ -303,9 +316,8 @@ export default class ElementWrapper extends Wrapper {
to_html((this.fragment.nodes as unknown as Array<ElementWrapper | TextWrapper>), block, literal, state, can_use_raw_text); to_html((this.fragment.nodes as unknown as Array<ElementWrapper | TextWrapper>), block, literal, state, can_use_raw_text);
literal.quasis.push(state.quasi); literal.quasis.push(state.quasi);
block.chunks.create.push( staticChildren = b`${node}.${this.can_use_innerhtml ? 'innerHTML' : 'textContent'} = ${literal};`;
b`${node}.${this.can_use_innerhtml ? 'innerHTML' : 'textContent'} = ${literal};` block.chunks.create.push(staticChildren);
);
} }
} else { } else {
this.fragment.nodes.forEach((child: Wrapper) => { this.fragment.nodes.forEach((child: Wrapper) => {
@ -334,6 +346,23 @@ export default class ElementWrapper extends Wrapper {
this.add_classes(block); this.add_classes(block);
this.add_manual_style_scoping(block); this.add_manual_style_scoping(block);
if (this.node.dynamic_tag) {
const dependencies = this.node.dynamic_tag.dynamic_dependencies();
if (dependencies.length) {
const condition = block.renderer.dirty(
dependencies
);
block.chunks.update.push(b`
if (${condition}) {
@detach(${node});
${node} = ${render_statement};
${staticChildren}
}
`);
}
}
if (nodes && this.renderer.options.hydratable && !this.void) { if (nodes && this.renderer.options.hydratable && !this.void) {
block.chunks.claim.push( block.chunks.claim.push(
b`${this.node.children.length > 0 ? nodes : children}.forEach(@detach);` b`${this.node.children.length > 0 ? nodes : children}.forEach(@detach);`

@ -181,7 +181,7 @@ export default function tag(parser: Parser) {
if (name === 'svelte:component') { if (name === 'svelte:component') {
const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'this'); const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'this');
if (!~index) { if (index === -1) {
parser.error({ parser.error({
code: 'missing-component-definition', code: 'missing-component-definition',
message: "<svelte:component> must have a 'this' attribute" message: "<svelte:component> must have a 'this' attribute"
@ -201,7 +201,7 @@ export default function tag(parser: Parser) {
if (name === 'svelte:element') { if (name === 'svelte:element') {
const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'tag'); const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'tag');
if (!~index) { if (index === -1) {
parser.error({ parser.error({
code: 'missing-element-definition', code: 'missing-element-definition',
message: '<svelte:element> must have a \'tag\' attribute' message: '<svelte:element> must have a \'tag\' attribute'

@ -97,6 +97,12 @@ export function validate_slots(name, slot, keys) {
} }
} }
export function validate_dynamic_element(tag) {
if (tag == null) {
console.warn('<svelte:element> expects a non-nullish value in attribute "tag"');
}
}
type Props = Record<string, any>; type Props = Record<string, any>;
export interface SvelteComponentDev { export interface SvelteComponentDev {
$set(props?: Props): void; $set(props?: Props): void;

@ -0,0 +1,9 @@
export default {
compileOptions: {
dev: true
},
warnings: [
'<svelte:element> expects a non-nullish value in attribute "tag"'
]
};

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

@ -0,0 +1,3 @@
export default {
error: "'value' is not a valid binding. svelte:element only supports bind:this"
};

@ -0,0 +1,6 @@
<script>
const tag = "div";
let value;
</script>
<svelte:element tag={tag} bind:value></svelte:element>

@ -0,0 +1,8 @@
export default {
html: '<div></div>',
test({ assert, component, target }) {
const div = target.querySelector('div');
assert.equal(div, component.foo);
}
};

@ -0,0 +1,6 @@
<script>
const tag = "div";
export let foo;
</script>
<svelte:element tag={tag} bind:this={foo}></svelte:element>

@ -0,0 +1,21 @@
let clicked = false;
function handler() {
clicked = true;
}
export default {
props: {
handler
},
html: '<button>Foo</button>',
test({ assert, target }) {
assert.equal(clicked, false);
const button = target.querySelector('button');
const click = new window.MouseEvent('click');
button.dispatchEvent(click);
assert.equal(clicked, true);
}
};

@ -0,0 +1,6 @@
<script>
const tag = "button";
export let handler;
</script>
<svelte:element tag={tag} on:click={handler}>Foo</svelte:element>

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

@ -0,0 +1,6 @@
<script>
import { writable } from 'svelte/store';
const foo = writable('div');
</script>
<svelte:element tag={$foo}></svelte:element>

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

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

@ -1,47 +0,0 @@
/* 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