diff --git a/src/compiler/compile/nodes/DynamicElement.ts b/src/compiler/compile/nodes/DynamicElement.ts index 8ec96f6eaf..bb59ad44c9 100644 --- a/src/compiler/compile/nodes/DynamicElement.ts +++ b/src/compiler/compile/nodes/DynamicElement.ts @@ -97,6 +97,19 @@ export default class DynamicElement extends Node { this.scope = scope; 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() { diff --git a/src/compiler/compile/render_dom/wrappers/DynamicElement.ts b/src/compiler/compile/render_dom/wrappers/DynamicElement.ts index f71ff8f0fa..62967b2827 100644 --- a/src/compiler/compile/render_dom/wrappers/DynamicElement.ts +++ b/src/compiler/compile/render_dom/wrappers/DynamicElement.ts @@ -1,7 +1,6 @@ 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'; @@ -10,7 +9,6 @@ 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; @@ -112,43 +110,45 @@ export default class DynamicElementWrapper extends Wrapper { ); 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} + if (has_transitions) { + block.chunks.intro.push(b`@transition_in(${this.var})`); + block.chunks.outro.push(b`@transition_out(${this.var})`); + + 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 { - ${this.var}.p(#ctx, #dirty); + block.chunks.update.push(b` + if (${condition}) { + ${body} + } + `); } - `); - } else { + } else if (dynamic) { block.chunks.update.push(b` + ${this.var}.p(#ctx, #dirty); 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)`); } } diff --git a/src/compiler/compile/render_dom/wrappers/Element/index.ts b/src/compiler/compile/render_dom/wrappers/Element/index.ts index 362c766221..c9ae8b9150 100644 --- a/src/compiler/compile/render_dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render_dom/wrappers/Element/index.ts @@ -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 (node.actions.length > 0 || node.animation || @@ -244,6 +248,14 @@ export default class ElementWrapper extends Wrapper { 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 (parent_nodes) { block.chunks.claim.push(b` @@ -278,13 +290,14 @@ export default class ElementWrapper extends Wrapper { block.chunks.destroy.push(b`if (detaching) @detach(${node});`); } + let staticChildren = null; + // insert static children with textContent or innerHTML 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.fragment.nodes.length === 1 && this.fragment.nodes[0].node.type === 'Text') { - block.chunks.create.push( - b`${node}.textContent = ${string_literal((this.fragment.nodes[0] as TextWrapper).data)};` - ); + staticChildren = b`${node}.textContent = ${string_literal((this.fragment.nodes[0] as TextWrapper).data)};`; + block.chunks.create.push(staticChildren); } else { const state = { quasi: { @@ -303,9 +316,8 @@ export default class ElementWrapper extends Wrapper { to_html((this.fragment.nodes as unknown as Array), block, literal, state, can_use_raw_text); literal.quasis.push(state.quasi); - block.chunks.create.push( - b`${node}.${this.can_use_innerhtml ? 'innerHTML' : 'textContent'} = ${literal};` - ); + staticChildren = b`${node}.${this.can_use_innerhtml ? 'innerHTML' : 'textContent'} = ${literal};`; + block.chunks.create.push(staticChildren); } } else { this.fragment.nodes.forEach((child: Wrapper) => { @@ -334,6 +346,23 @@ export default class ElementWrapper extends Wrapper { this.add_classes(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) { block.chunks.claim.push( b`${this.node.children.length > 0 ? nodes : children}.forEach(@detach);` diff --git a/src/compiler/parse/state/tag.ts b/src/compiler/parse/state/tag.ts index a693426935..dada08b686 100644 --- a/src/compiler/parse/state/tag.ts +++ b/src/compiler/parse/state/tag.ts @@ -181,7 +181,7 @@ export default function tag(parser: Parser) { if (name === 'svelte:component') { const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'this'); - if (!~index) { + if (index === -1) { parser.error({ code: 'missing-component-definition', message: " must have a 'this' attribute" @@ -201,7 +201,7 @@ export default function tag(parser: Parser) { if (name === 'svelte:element') { const index = element.attributes.findIndex(attr => attr.type === 'Attribute' && attr.name === 'tag'); - if (!~index) { + if (index === -1) { parser.error({ code: 'missing-element-definition', message: ' must have a \'tag\' attribute' diff --git a/src/runtime/internal/dev.ts b/src/runtime/internal/dev.ts index 99ff067474..bfb3d795f2 100644 --- a/src/runtime/internal/dev.ts +++ b/src/runtime/internal/dev.ts @@ -97,6 +97,12 @@ export function validate_slots(name, slot, keys) { } } +export function validate_dynamic_element(tag) { + if (tag == null) { + console.warn(' expects a non-nullish value in attribute "tag"'); + } +} + type Props = Record; export interface SvelteComponentDev { $set(props?: Props): void; diff --git a/test/runtime/samples/dev-warning-dynamic-element-nullish-tag/_config.js b/test/runtime/samples/dev-warning-dynamic-element-nullish-tag/_config.js new file mode 100644 index 0000000000..026a93cf74 --- /dev/null +++ b/test/runtime/samples/dev-warning-dynamic-element-nullish-tag/_config.js @@ -0,0 +1,9 @@ +export default { + compileOptions: { + dev: true + }, + + warnings: [ + ' expects a non-nullish value in attribute "tag"' + ] +}; diff --git a/test/runtime/samples/dev-warning-dynamic-element-nullish-tag/main.svelte b/test/runtime/samples/dev-warning-dynamic-element-nullish-tag/main.svelte new file mode 100644 index 0000000000..8ab231d49d --- /dev/null +++ b/test/runtime/samples/dev-warning-dynamic-element-nullish-tag/main.svelte @@ -0,0 +1,5 @@ + + + diff --git a/test/runtime/samples/dynamic-element-binding-invalid/_config.js b/test/runtime/samples/dynamic-element-binding-invalid/_config.js new file mode 100644 index 0000000000..33a48a0e85 --- /dev/null +++ b/test/runtime/samples/dynamic-element-binding-invalid/_config.js @@ -0,0 +1,3 @@ +export default { + error: "'value' is not a valid binding. svelte:element only supports bind:this" +}; diff --git a/test/runtime/samples/dynamic-element-binding-invalid/main.svelte b/test/runtime/samples/dynamic-element-binding-invalid/main.svelte new file mode 100644 index 0000000000..4639427fdf --- /dev/null +++ b/test/runtime/samples/dynamic-element-binding-invalid/main.svelte @@ -0,0 +1,6 @@ + + + diff --git a/test/runtime/samples/dynamic-element-binding-this/_config.js b/test/runtime/samples/dynamic-element-binding-this/_config.js new file mode 100644 index 0000000000..e0722c9375 --- /dev/null +++ b/test/runtime/samples/dynamic-element-binding-this/_config.js @@ -0,0 +1,8 @@ +export default { + html: '
', + + test({ assert, component, target }) { + const div = target.querySelector('div'); + assert.equal(div, component.foo); + } +}; diff --git a/test/runtime/samples/dynamic-element-binding-this/main.svelte b/test/runtime/samples/dynamic-element-binding-this/main.svelte new file mode 100644 index 0000000000..0c1e3a3f15 --- /dev/null +++ b/test/runtime/samples/dynamic-element-binding-this/main.svelte @@ -0,0 +1,6 @@ + + + diff --git a/test/runtime/samples/dynamic-element-event-handler/_config.js b/test/runtime/samples/dynamic-element-event-handler/_config.js new file mode 100644 index 0000000000..03b8f7879d --- /dev/null +++ b/test/runtime/samples/dynamic-element-event-handler/_config.js @@ -0,0 +1,21 @@ +let clicked = false; +function handler() { + clicked = true; +} + +export default { + props: { + handler + }, + html: '', + + 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); + } +}; diff --git a/test/runtime/samples/dynamic-element-event-handler/main.svelte b/test/runtime/samples/dynamic-element-event-handler/main.svelte new file mode 100644 index 0000000000..1fd8c41dcf --- /dev/null +++ b/test/runtime/samples/dynamic-element-event-handler/main.svelte @@ -0,0 +1,6 @@ + + +Foo \ No newline at end of file diff --git a/test/runtime/samples/dynamic-element-change-tag-reuse-children/_config.js b/test/runtime/samples/dynamic-element-reuse-children/_config.js similarity index 100% rename from test/runtime/samples/dynamic-element-change-tag-reuse-children/_config.js rename to test/runtime/samples/dynamic-element-reuse-children/_config.js diff --git a/test/runtime/samples/dynamic-element-change-tag-reuse-children/main.svelte b/test/runtime/samples/dynamic-element-reuse-children/main.svelte similarity index 100% rename from test/runtime/samples/dynamic-element-change-tag-reuse-children/main.svelte rename to test/runtime/samples/dynamic-element-reuse-children/main.svelte diff --git a/test/runtime/samples/dynamic-element-store/_config.js b/test/runtime/samples/dynamic-element-store/_config.js new file mode 100644 index 0000000000..ded19eef79 --- /dev/null +++ b/test/runtime/samples/dynamic-element-store/_config.js @@ -0,0 +1,3 @@ +export default { + html: '
' +}; diff --git a/test/runtime/samples/dynamic-element-store/main.svelte b/test/runtime/samples/dynamic-element-store/main.svelte new file mode 100644 index 0000000000..5b8098d244 --- /dev/null +++ b/test/runtime/samples/dynamic-element-store/main.svelte @@ -0,0 +1,6 @@ + + + \ No newline at end of file diff --git a/test/runtime/samples/dynamic-element-variable/_config.js b/test/runtime/samples/dynamic-element-variable/_config.js index 76dd0e606e..20e0fa9418 100644 --- a/test/runtime/samples/dynamic-element-variable/_config.js +++ b/test/runtime/samples/dynamic-element-variable/_config.js @@ -1,13 +1,13 @@ export default { props: { - tag: 'di', + tag: 'div', text: 'Foo' }, html: '
Foo
', test({ assert, component, target }) { const div = target.firstChild; - component.tag = 'na'; + component.tag = 'nav'; component.text = 'Bar'; assert.htmlEqual(target.innerHTML, ` diff --git a/test/runtime/samples/dynamic-element-variable/main.svelte b/test/runtime/samples/dynamic-element-variable/main.svelte index 8e94f12e11..4cac422635 100644 --- a/test/runtime/samples/dynamic-element-variable/main.svelte +++ b/test/runtime/samples/dynamic-element-variable/main.svelte @@ -1,6 +1,6 @@ -{text} \ No newline at end of file +{text} \ No newline at end of file diff --git a/test/sourcemaps/samples/two-scripts/output.js b/test/sourcemaps/samples/two-scripts/output.js deleted file mode 100644 index 3d6fd51b85..0000000000 --- a/test/sourcemaps/samples/two-scripts/output.js +++ /dev/null @@ -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 \ No newline at end of file