From a0f0e8ad677f5663520ee4a815e375c34b44b505 Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Tue, 15 Sep 2020 15:41:16 +0800 Subject: [PATCH] key block --- src/compiler/compile/nodes/KeyBlock.ts | 35 +++++ src/compiler/compile/nodes/interfaces.ts | 2 + .../compile/nodes/shared/map_children.ts | 2 + .../compile/render_dom/wrappers/Fragment.ts | 2 + .../compile/render_dom/wrappers/KeyBlock.ts | 120 ++++++++++++++++++ src/compiler/compile/render_ssr/Renderer.ts | 2 + .../compile/render_ssr/handlers/KeyBlock.ts | 6 + src/compiler/parse/state/mustache.ts | 6 +- .../samples/key-block-multiple/_config.js | 21 +++ .../samples/key-block-multiple/main.svelte | 9 ++ .../samples/key-block-static/_config.js | 9 ++ .../samples/key-block-static/main.svelte | 8 ++ .../samples/key-block-transition/_config.js | 24 ++++ .../samples/key-block-transition/main.svelte | 17 +++ test/runtime/samples/key-block/_config.js | 13 ++ test/runtime/samples/key-block/main.svelte | 8 ++ 16 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 src/compiler/compile/nodes/KeyBlock.ts create mode 100644 src/compiler/compile/render_dom/wrappers/KeyBlock.ts create mode 100644 src/compiler/compile/render_ssr/handlers/KeyBlock.ts create mode 100644 test/runtime/samples/key-block-multiple/_config.js create mode 100644 test/runtime/samples/key-block-multiple/main.svelte create mode 100644 test/runtime/samples/key-block-static/_config.js create mode 100644 test/runtime/samples/key-block-static/main.svelte create mode 100644 test/runtime/samples/key-block-transition/_config.js create mode 100644 test/runtime/samples/key-block-transition/main.svelte create mode 100644 test/runtime/samples/key-block/_config.js create mode 100644 test/runtime/samples/key-block/main.svelte diff --git a/src/compiler/compile/nodes/KeyBlock.ts b/src/compiler/compile/nodes/KeyBlock.ts new file mode 100644 index 0000000000..906f9f7ce9 --- /dev/null +++ b/src/compiler/compile/nodes/KeyBlock.ts @@ -0,0 +1,35 @@ +import Expression from "./shared/Expression"; +import map_children from "./shared/map_children"; +import AbstractBlock from "./shared/AbstractBlock"; +import Element from "./Element"; + +export default class KeyBlock extends AbstractBlock { + type: "KeyBlock"; + + expression: Expression; + has_animation: boolean; + + constructor(component, parent, scope, info) { + super(component, parent, scope, info); + + this.expression = new Expression(component, this, scope, info.expression); + + this.has_animation = false; + + this.children = map_children(component, this, scope, info.children); + + if (this.has_animation) { + if (this.children.length !== 1) { + const child = this.children.find( + (child) => !!(child as Element).animation + ); + component.error((child as Element).animation, { + code: `invalid-animation`, + message: `An element that use the animate directive must be the sole child of a key block` + }); + } + } + + this.warn_if_empty_block(); + } +} diff --git a/src/compiler/compile/nodes/interfaces.ts b/src/compiler/compile/nodes/interfaces.ts index 752168a49d..51d7b17e07 100644 --- a/src/compiler/compile/nodes/interfaces.ts +++ b/src/compiler/compile/nodes/interfaces.ts @@ -18,6 +18,7 @@ import Fragment from './Fragment'; import Head from './Head'; import IfBlock from './IfBlock'; import InlineComponent from './InlineComponent'; +import KeyBlock from './KeyBlock'; import Let from './Let'; import MustacheTag from './MustacheTag'; import Options from './Options'; @@ -50,6 +51,7 @@ export type INode = Action | Head | IfBlock | InlineComponent +| KeyBlock | Let | MustacheTag | Options diff --git a/src/compiler/compile/nodes/shared/map_children.ts b/src/compiler/compile/nodes/shared/map_children.ts index dcdc52f86d..5d5da223fb 100644 --- a/src/compiler/compile/nodes/shared/map_children.ts +++ b/src/compiler/compile/nodes/shared/map_children.ts @@ -6,6 +6,7 @@ import Element from '../Element'; import Head from '../Head'; import IfBlock from '../IfBlock'; import InlineComponent from '../InlineComponent'; +import KeyBlock from '../KeyBlock'; import MustacheTag from '../MustacheTag'; import Options from '../Options'; import RawMustacheTag from '../RawMustacheTag'; @@ -28,6 +29,7 @@ function get_constructor(type) { case 'Head': return Head; case 'IfBlock': return IfBlock; case 'InlineComponent': return InlineComponent; + case 'KeyBlock': return KeyBlock; case 'MustacheTag': return MustacheTag; case 'Options': return Options; case 'RawMustacheTag': return RawMustacheTag; diff --git a/src/compiler/compile/render_dom/wrappers/Fragment.ts b/src/compiler/compile/render_dom/wrappers/Fragment.ts index a0984b69b9..853a41f3cc 100644 --- a/src/compiler/compile/render_dom/wrappers/Fragment.ts +++ b/src/compiler/compile/render_dom/wrappers/Fragment.ts @@ -6,6 +6,7 @@ import EachBlock from './EachBlock'; import Element from './Element/index'; import Head from './Head'; import IfBlock from './IfBlock'; +import KeyBlock from './KeyBlock'; import InlineComponent from './InlineComponent/index'; import MustacheTag from './MustacheTag'; import RawMustacheTag from './RawMustacheTag'; @@ -30,6 +31,7 @@ const wrappers = { Head, IfBlock, InlineComponent, + KeyBlock, MustacheTag, Options: null, RawMustacheTag, diff --git a/src/compiler/compile/render_dom/wrappers/KeyBlock.ts b/src/compiler/compile/render_dom/wrappers/KeyBlock.ts new file mode 100644 index 0000000000..1f33b39196 --- /dev/null +++ b/src/compiler/compile/render_dom/wrappers/KeyBlock.ts @@ -0,0 +1,120 @@ +import Wrapper from "./shared/Wrapper"; +import Renderer from "../Renderer"; +import Block from "../Block"; +import EachBlock from "../../nodes/EachBlock"; +import KeyBlock from "../../nodes/KeyBlock"; +import create_debugging_comment from "./shared/create_debugging_comment"; +import FragmentWrapper from "./Fragment"; +import { b, x } from "code-red"; +import { Identifier } from "estree"; + +export default class KeyBlockWrapper extends Wrapper { + node: KeyBlock; + fragment: FragmentWrapper; + block: Block; + dependencies: string[]; + var: Identifier = { type: "Identifier", name: "key_block" }; + + constructor( + renderer: Renderer, + block: Block, + parent: Wrapper, + node: EachBlock, + strip_whitespace: boolean, + next_sibling: Wrapper + ) { + super(renderer, block, parent, node); + + this.cannot_use_innerhtml(); + this.not_static_content(); + + this.dependencies = node.expression.dynamic_dependencies(); + + this.block = block.child({ + comment: create_debugging_comment(node, renderer.component), + name: renderer.component.get_unique_name("create_key_block"), + type: "key" + }); + + this.fragment = new FragmentWrapper( + renderer, + this.block, + node.children, + parent, + strip_whitespace, + next_sibling + ); + + renderer.blocks.push(this.block); + } + + render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { + this.fragment.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; + + 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); + + if (this.dependencies.length) { + 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 (${this.renderer.dirty(this.dependencies)}) { + ${body} + } else { + ${this.var}.p(#ctx, #dirty); + } + `); + } else { + block.chunks.update.push(b` + if (${this.renderer.dirty(this.dependencies)}) { + ${body} + } + `); + } + } else if (dynamic) { + block.chunks.update.push(b`${this.var}.p(#ctx, #dirty);`); + } + + 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_ssr/Renderer.ts b/src/compiler/compile/render_ssr/Renderer.ts index fb9216327c..c633ff8b0a 100644 --- a/src/compiler/compile/render_ssr/Renderer.ts +++ b/src/compiler/compile/render_ssr/Renderer.ts @@ -7,6 +7,7 @@ import Head from './handlers/Head'; import HtmlTag from './handlers/HtmlTag'; import IfBlock from './handlers/IfBlock'; import InlineComponent from './handlers/InlineComponent'; +import KeyBlock from './handlers/KeyBlock'; import Slot from './handlers/Slot'; import Tag from './handlers/Tag'; import Text from './handlers/Text'; @@ -30,6 +31,7 @@ const handlers: Record = { Head, IfBlock, InlineComponent, + KeyBlock, MustacheTag: Tag, // TODO MustacheTag is an anachronism Options: noop, RawMustacheTag: HtmlTag, diff --git a/src/compiler/compile/render_ssr/handlers/KeyBlock.ts b/src/compiler/compile/render_ssr/handlers/KeyBlock.ts new file mode 100644 index 0000000000..33a6681280 --- /dev/null +++ b/src/compiler/compile/render_ssr/handlers/KeyBlock.ts @@ -0,0 +1,6 @@ +import KeyBlock from '../../nodes/KeyBlock'; +import Renderer, { RenderOptions } from '../Renderer'; + +export default function(node: KeyBlock, renderer: Renderer, options: RenderOptions) { + renderer.render(node.children, options); +} diff --git a/src/compiler/parse/state/mustache.ts b/src/compiler/parse/state/mustache.ts index dc26d994df..93240491bd 100644 --- a/src/compiler/parse/state/mustache.ts +++ b/src/compiler/parse/state/mustache.ts @@ -38,7 +38,7 @@ export default function mustache(parser: Parser) { parser.allow_whitespace(); - // {/if}, {/each} or {/await} + // {/if}, {/each}, {/await} or {/key} if (parser.eat('/')) { let block = parser.current(); let expected; @@ -63,6 +63,8 @@ export default function mustache(parser: Parser) { expected = 'each'; } else if (block.type === 'AwaitBlock') { expected = 'await'; + } else if (block.type === 'KeyBlock') { + expected = 'key'; } else { parser.error({ code: `unexpected-block-close`, @@ -221,6 +223,8 @@ export default function mustache(parser: Parser) { type = 'EachBlock'; } else if (parser.eat('await')) { type = 'AwaitBlock'; + } else if (parser.eat('key')) { + type = 'KeyBlock'; } else { parser.error({ code: `expected-block-type`, diff --git a/test/runtime/samples/key-block-multiple/_config.js b/test/runtime/samples/key-block-multiple/_config.js new file mode 100644 index 0000000000..7a8314d8ff --- /dev/null +++ b/test/runtime/samples/key-block-multiple/_config.js @@ -0,0 +1,21 @@ +export default { + html: `
000
`, + async test({ assert, component, target, window }) { + let div = target.querySelector('div'); + component.value = 2; + assert.htmlEqual(target.innerHTML, `
200
`); + assert.notStrictEqual(div, target.querySelector('div')); + + div = target.querySelector('div'); + + component.anotherValue = 5; + assert.htmlEqual(target.innerHTML, `
250
`); + assert.notStrictEqual(div, target.querySelector('div')); + + div = target.querySelector('div'); + + component.thirdValue = 9; + assert.htmlEqual(target.innerHTML, `
259
`); + assert.strictEqual(div, target.querySelector('div')); + } +}; diff --git a/test/runtime/samples/key-block-multiple/main.svelte b/test/runtime/samples/key-block-multiple/main.svelte new file mode 100644 index 0000000000..88c9213506 --- /dev/null +++ b/test/runtime/samples/key-block-multiple/main.svelte @@ -0,0 +1,9 @@ + + +{#key [value, anotherValue]} +
{value}{anotherValue}{thirdValue}
+{/key} \ No newline at end of file diff --git a/test/runtime/samples/key-block-static/_config.js b/test/runtime/samples/key-block-static/_config.js new file mode 100644 index 0000000000..d5ea0bf687 --- /dev/null +++ b/test/runtime/samples/key-block-static/_config.js @@ -0,0 +1,9 @@ +export default { + html: `
00
`, + async test({ assert, component, target, window }) { + const div = target.querySelector('div'); + component.anotherValue = 2; + assert.htmlEqual(target.innerHTML, `
02
`); + assert.strictEqual(div, target.querySelector('div')); + } +}; diff --git a/test/runtime/samples/key-block-static/main.svelte b/test/runtime/samples/key-block-static/main.svelte new file mode 100644 index 0000000000..e4ee6b5d71 --- /dev/null +++ b/test/runtime/samples/key-block-static/main.svelte @@ -0,0 +1,8 @@ + + +{#key value} +
{value}{anotherValue}
+{/key} \ No newline at end of file diff --git a/test/runtime/samples/key-block-transition/_config.js b/test/runtime/samples/key-block-transition/_config.js new file mode 100644 index 0000000000..53de6b333c --- /dev/null +++ b/test/runtime/samples/key-block-transition/_config.js @@ -0,0 +1,24 @@ +export default { + html: '
0
', + async test({ assert, component, target, window, raf }) { + component.value = 2; + + const [div1, div2] = target.querySelectorAll('div'); + + assert.htmlEqual(div1.outerHTML, '
0
'); + assert.htmlEqual(div2.outerHTML, '
2
'); + + raf.tick(0); + + assert.equal(div1.foo, 1); + assert.equal(div1.oof, 0); + + assert.equal(div2.foo, 0); + assert.equal(div2.oof, 1); + + raf.tick(200); + + assert.htmlEqual(target.innerHTML, '
2
'); + assert.equal(div2, target.querySelector('div')); + } +}; diff --git a/test/runtime/samples/key-block-transition/main.svelte b/test/runtime/samples/key-block-transition/main.svelte new file mode 100644 index 0000000000..d7fb6ec024 --- /dev/null +++ b/test/runtime/samples/key-block-transition/main.svelte @@ -0,0 +1,17 @@ + + +{#key value} +
{value}
+{/key} \ No newline at end of file diff --git a/test/runtime/samples/key-block/_config.js b/test/runtime/samples/key-block/_config.js new file mode 100644 index 0000000000..8524117c8d --- /dev/null +++ b/test/runtime/samples/key-block/_config.js @@ -0,0 +1,13 @@ +export default { + html: `
00
`, + async test({ assert, component, target, window }) { + const div = target.querySelector('div'); + component.reactive = 2; + assert.htmlEqual(target.innerHTML, `
02
`); + assert.strictEqual(div, target.querySelector('div')); + + component.value = 5; + assert.htmlEqual(target.innerHTML, `
52
`); + assert.notStrictEqual(div, target.querySelector('div')); + } +}; diff --git a/test/runtime/samples/key-block/main.svelte b/test/runtime/samples/key-block/main.svelte new file mode 100644 index 0000000000..466d20b10a --- /dev/null +++ b/test/runtime/samples/key-block/main.svelte @@ -0,0 +1,8 @@ + + +{#key value} +
{value}{reactive}
+{/key} \ No newline at end of file