From a0f0e8ad677f5663520ee4a815e375c34b44b505 Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Tue, 15 Sep 2020 15:41:16 +0800 Subject: [PATCH 1/8] 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 000000000000..906f9f7ce9e7 --- /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 752168a49d41..51d7b17e07b7 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 dcdc52f86d16..5d5da223fb02 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 a0984b69b920..853a41f3cc64 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 000000000000..1f33b3919605 --- /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 fb9216327c68..c633ff8b0ab5 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 000000000000..33a6681280d9 --- /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 dc26d994df97..93240491bd44 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 000000000000..7a8314d8ff1b --- /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 000000000000..88c921350600 --- /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 000000000000..d5ea0bf6871e --- /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 000000000000..e4ee6b5d7146 --- /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 000000000000..53de6b333cf0 --- /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 000000000000..d7fb6ec02471 --- /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 000000000000..8524117c8df0 --- /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 000000000000..466d20b10a73 --- /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 From e3112a471174f7f1366e3e69b389e99d32358694 Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Wed, 16 Sep 2020 16:13:50 +0800 Subject: [PATCH 2/8] only recreate when the expression value changed --- .../compile/render_dom/wrappers/KeyBlock.ts | 96 +++++++++++-------- test/runtime/samples/key-block-2/_config.js | 14 +++ test/runtime/samples/key-block-2/main.svelte | 8 ++ .../key-block-array-immutable/_config.js | 15 +++ .../key-block-array-immutable/main.svelte | 14 +++ .../samples/key-block-array/_config.js | 15 +++ .../samples/key-block-array/main.svelte | 12 +++ .../samples/key-block-expression-2/_config.js | 18 ++++ .../key-block-expression-2/main.svelte | 17 ++++ .../samples/key-block-expression/_config.js | 28 ++++++ .../main.svelte | 2 +- .../samples/key-block-multiple/_config.js | 21 ---- test/runtime/samples/key-block/_config.js | 18 ++-- test/runtime/samples/key-block/main.svelte | 6 +- 14 files changed, 213 insertions(+), 71 deletions(-) create mode 100644 test/runtime/samples/key-block-2/_config.js create mode 100644 test/runtime/samples/key-block-2/main.svelte create mode 100644 test/runtime/samples/key-block-array-immutable/_config.js create mode 100644 test/runtime/samples/key-block-array-immutable/main.svelte create mode 100644 test/runtime/samples/key-block-array/_config.js create mode 100644 test/runtime/samples/key-block-array/main.svelte create mode 100644 test/runtime/samples/key-block-expression-2/_config.js create mode 100644 test/runtime/samples/key-block-expression-2/main.svelte create mode 100644 test/runtime/samples/key-block-expression/_config.js rename test/runtime/samples/{key-block-multiple => key-block-expression}/main.svelte (84%) delete mode 100644 test/runtime/samples/key-block-multiple/_config.js diff --git a/src/compiler/compile/render_dom/wrappers/KeyBlock.ts b/src/compiler/compile/render_dom/wrappers/KeyBlock.ts index 1f33b3919605..359fb6946f27 100644 --- a/src/compiler/compile/render_dom/wrappers/KeyBlock.ts +++ b/src/compiler/compile/render_dom/wrappers/KeyBlock.ts @@ -30,12 +30,16 @@ export default class KeyBlockWrapper extends Wrapper { 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" - }); + if (this.dependencies.length) { + block = block.child({ + comment: create_debugging_comment(node, renderer.component), + name: renderer.component.get_unique_name("create_key_block"), + type: "key" + }); + renderer.blocks.push(block); + } + this.block = block; this.fragment = new FragmentWrapper( renderer, this.block, @@ -44,11 +48,21 @@ export default class KeyBlockWrapper extends Wrapper { strip_whitespace, next_sibling ); - - renderer.blocks.push(this.block); } render(block: Block, parent_node: Identifier, parent_nodes: Identifier) { + if (this.dependencies.length === 0) { + this.render_static_key(block, parent_node, parent_nodes); + } else { + this.render_dynamic_key(block, parent_node, parent_nodes); + } + } + + render_static_key(_block: Block, parent_node: Identifier, parent_nodes: Identifier) { + this.fragment.render(this.block, parent_node, parent_nodes); + } + + render_dynamic_key(block: Block, parent_node: Identifier, parent_nodes: Identifier) { this.fragment.render( this.block, null, @@ -60,6 +74,13 @@ export default class KeyBlockWrapper extends Wrapper { ); const dynamic = this.block.has_update_method; + const previous_key = block.get_unique_name('previous_key'); + const snippet = this.node.expression.manipulate(block); + block.add_variable(previous_key, 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_key}, ${previous_key} = ${snippet})`; + block.chunks.init.push(b` let ${this.var} = ${this.block.name}(#ctx); `); @@ -73,41 +94,36 @@ export default class KeyBlockWrapper 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 (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);` + if (dynamic) { + block.chunks.update.push(b` + if (${condition}) { + ${body} + } else { + ${this.var}.p(#ctx, #dirty); } - ${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);`); + `); + } else { + block.chunks.update.push(b` + if (${condition}) { + ${body} + } + `); } if (has_transitions) { diff --git a/test/runtime/samples/key-block-2/_config.js b/test/runtime/samples/key-block-2/_config.js new file mode 100644 index 000000000000..a7c53bd91fb4 --- /dev/null +++ b/test/runtime/samples/key-block-2/_config.js @@ -0,0 +1,14 @@ +// with reactive content beside `key` +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-2/main.svelte b/test/runtime/samples/key-block-2/main.svelte new file mode 100644 index 000000000000..466d20b10a73 --- /dev/null +++ b/test/runtime/samples/key-block-2/main.svelte @@ -0,0 +1,8 @@ + + +{#key value} +
{value}{reactive}
+{/key} \ No newline at end of file diff --git a/test/runtime/samples/key-block-array-immutable/_config.js b/test/runtime/samples/key-block-array-immutable/_config.js new file mode 100644 index 000000000000..fb94556c0ff7 --- /dev/null +++ b/test/runtime/samples/key-block-array-immutable/_config.js @@ -0,0 +1,15 @@ +export default { + html: `
1
`, + async test({ assert, component, target, window }) { + let div = target.querySelector("div"); + await component.append(2); + assert.htmlEqual(target.innerHTML, `
1
`); + assert.strictEqual(div, target.querySelector("div")); + + div = target.querySelector("div"); + + component.array = [3, 4]; + assert.htmlEqual(target.innerHTML, `
3,4
`); + assert.notStrictEqual(div, target.querySelector("div")); + } +}; diff --git a/test/runtime/samples/key-block-array-immutable/main.svelte b/test/runtime/samples/key-block-array-immutable/main.svelte new file mode 100644 index 000000000000..e666275af4c7 --- /dev/null +++ b/test/runtime/samples/key-block-array-immutable/main.svelte @@ -0,0 +1,14 @@ + + + + +{#key array} +
{array.join(',')}
+{/key} \ No newline at end of file diff --git a/test/runtime/samples/key-block-array/_config.js b/test/runtime/samples/key-block-array/_config.js new file mode 100644 index 000000000000..05d5fe9995f4 --- /dev/null +++ b/test/runtime/samples/key-block-array/_config.js @@ -0,0 +1,15 @@ +export default { + html: `
1
`, + async test({ assert, component, target, window }) { + let div = target.querySelector("div"); + await component.append(2); + assert.htmlEqual(target.innerHTML, `
1,2
`); + assert.notStrictEqual(div, target.querySelector("div")); + + div = target.querySelector("div"); + + component.array = [3, 4]; + assert.htmlEqual(target.innerHTML, `
3,4
`); + assert.notStrictEqual(div, target.querySelector("div")); + } +}; diff --git a/test/runtime/samples/key-block-array/main.svelte b/test/runtime/samples/key-block-array/main.svelte new file mode 100644 index 000000000000..5a4054b0430d --- /dev/null +++ b/test/runtime/samples/key-block-array/main.svelte @@ -0,0 +1,12 @@ + + +{#key array} +
{array.join(',')}
+{/key} \ No newline at end of file diff --git a/test/runtime/samples/key-block-expression-2/_config.js b/test/runtime/samples/key-block-expression-2/_config.js new file mode 100644 index 000000000000..236c72fa3dd5 --- /dev/null +++ b/test/runtime/samples/key-block-expression-2/_config.js @@ -0,0 +1,18 @@ +export default { + html: `
3
`, + async test({ assert, component, target, window }) { + const div = target.querySelector("div"); + + await component.mutate(); + assert.htmlEqual(target.innerHTML, `
5
`); + assert.strictEqual(div, target.querySelector("div")); + + await component.reassign(); + assert.htmlEqual(target.innerHTML, `
7
`); + assert.strictEqual(div, target.querySelector("div")); + + await component.changeKey(); + assert.htmlEqual(target.innerHTML, `
7
`); + assert.notStrictEqual(div, target.querySelector("div")); + } +}; diff --git a/test/runtime/samples/key-block-expression-2/main.svelte b/test/runtime/samples/key-block-expression-2/main.svelte new file mode 100644 index 000000000000..5525f637615f --- /dev/null +++ b/test/runtime/samples/key-block-expression-2/main.svelte @@ -0,0 +1,17 @@ + + +{#key obj.key} +
{obj.value}
+{/key} \ No newline at end of file diff --git a/test/runtime/samples/key-block-expression/_config.js b/test/runtime/samples/key-block-expression/_config.js new file mode 100644 index 000000000000..78890988eab6 --- /dev/null +++ b/test/runtime/samples/key-block-expression/_config.js @@ -0,0 +1,28 @@ +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")); + + // make dirty while maintain the value of `value + anotherValue` + // should update the content, but not recreate the elements + await component.$set({ value: 4, anotherValue: 3 }); + + assert.htmlEqual(target.innerHTML, `
439
`); + assert.strictEqual(div, target.querySelector("div")); + } +}; diff --git a/test/runtime/samples/key-block-multiple/main.svelte b/test/runtime/samples/key-block-expression/main.svelte similarity index 84% rename from test/runtime/samples/key-block-multiple/main.svelte rename to test/runtime/samples/key-block-expression/main.svelte index 88c921350600..dd752e8b8fdf 100644 --- a/test/runtime/samples/key-block-multiple/main.svelte +++ b/test/runtime/samples/key-block-expression/main.svelte @@ -4,6 +4,6 @@ export let thirdValue = 0; -{#key [value, anotherValue]} +{#key value + anotherValue}
{value}{anotherValue}{thirdValue}
{/key} \ No newline at end of file diff --git a/test/runtime/samples/key-block-multiple/_config.js b/test/runtime/samples/key-block-multiple/_config.js deleted file mode 100644 index 7a8314d8ff1b..000000000000 --- a/test/runtime/samples/key-block-multiple/_config.js +++ /dev/null @@ -1,21 +0,0 @@ -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/_config.js b/test/runtime/samples/key-block/_config.js index 8524117c8df0..ad206c3b06da 100644 --- a/test/runtime/samples/key-block/_config.js +++ b/test/runtime/samples/key-block/_config.js @@ -1,13 +1,17 @@ export default { - html: `
00
`, + html: `
0
0
`, 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')); + let [div1, div2] = target.querySelectorAll('div'); component.value = 5; - assert.htmlEqual(target.innerHTML, `
52
`); - assert.notStrictEqual(div, target.querySelector('div')); + assert.htmlEqual(target.innerHTML, `
5
0
`); + assert.notStrictEqual(div1, target.querySelectorAll('div')[0]); + assert.strictEqual(div2, target.querySelectorAll('div')[1]); + [div1, div2] = target.querySelectorAll('div'); + + component.reactive = 10; + assert.htmlEqual(target.innerHTML, `
5
10
`); + assert.strictEqual(div1, target.querySelectorAll('div')[0]); + assert.strictEqual(div2, target.querySelectorAll('div')[1]); } }; diff --git a/test/runtime/samples/key-block/main.svelte b/test/runtime/samples/key-block/main.svelte index 466d20b10a73..ac3c340770b4 100644 --- a/test/runtime/samples/key-block/main.svelte +++ b/test/runtime/samples/key-block/main.svelte @@ -4,5 +4,7 @@ {#key value} -
{value}{reactive}
-{/key} \ No newline at end of file +
{value}
+{/key} + +
{reactive}
\ No newline at end of file From 0d206e2600a0116085d340157d968f2e1867a301 Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Wed, 23 Sep 2020 09:47:08 +0800 Subject: [PATCH 3/8] remove copypaste animation validation from keyblock --- src/compiler/compile/nodes/KeyBlock.ts | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/compiler/compile/nodes/KeyBlock.ts b/src/compiler/compile/nodes/KeyBlock.ts index 906f9f7ce9e7..356210b6b0f3 100644 --- a/src/compiler/compile/nodes/KeyBlock.ts +++ b/src/compiler/compile/nodes/KeyBlock.ts @@ -1,35 +1,19 @@ 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(); } } From 061525a565817e6191573580023d37fb5e7a222e Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Wed, 23 Sep 2020 10:17:42 +0800 Subject: [PATCH 4/8] add docs --- site/content/docs/02-template-syntax.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/site/content/docs/02-template-syntax.md b/site/content/docs/02-template-syntax.md index e93449728bfc..74beaa2358e9 100644 --- a/site/content/docs/02-template-syntax.md +++ b/site/content/docs/02-template-syntax.md @@ -342,6 +342,31 @@ If you don't care about the pending state, you can also omit the initial block. {/await} ``` +### {#key ...} + +```sv +{#key expression}...{/key} +``` + +--- + +Key block destroys and recreates the elements when the value of the expression changes. + +This is useful if you want to transition the element whenever the expression value changes. + +```sv +{#key value} +
{value}
+{/key} +``` + +Another use case is to reinitialise a Svelte component. + +```sv +{#key value} + +{/key} +``` ### {@html ...} From 92c5a8aa2159c06a4e3da9d41e7e1818afed9b38 Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Fri, 25 Sep 2020 10:29:32 +0800 Subject: [PATCH 5/8] update error message --- src/compiler/parse/state/mustache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compiler/parse/state/mustache.ts b/src/compiler/parse/state/mustache.ts index 93240491bd44..b72b77c30bd0 100644 --- a/src/compiler/parse/state/mustache.ts +++ b/src/compiler/parse/state/mustache.ts @@ -228,7 +228,7 @@ export default function mustache(parser: Parser) { } else { parser.error({ code: `expected-block-type`, - message: `Expected if, each or await` + message: `Expected if, each, await or key` }); } From 96792058b968e08c4006188669561eb3c0dd362e Mon Sep 17 00:00:00 2001 From: Tan Li Hau Date: Fri, 25 Sep 2020 10:35:25 +0800 Subject: [PATCH 6/8] add another test case --- test/runtime/samples/key-block-3/_config.js | 11 +++++++++++ test/runtime/samples/key-block-3/main.svelte | 7 +++++++ 2 files changed, 18 insertions(+) create mode 100644 test/runtime/samples/key-block-3/_config.js create mode 100644 test/runtime/samples/key-block-3/main.svelte diff --git a/test/runtime/samples/key-block-3/_config.js b/test/runtime/samples/key-block-3/_config.js new file mode 100644 index 000000000000..4290599cb37d --- /dev/null +++ b/test/runtime/samples/key-block-3/_config.js @@ -0,0 +1,11 @@ +// key is not used in the template +export default { + html: `
`, + async test({ assert, component, target, window }) { + const div = target.querySelector('div'); + + component.value = 5; + assert.htmlEqual(target.innerHTML, `
`); + assert.notStrictEqual(div, target.querySelector('div')); + } +}; diff --git a/test/runtime/samples/key-block-3/main.svelte b/test/runtime/samples/key-block-3/main.svelte new file mode 100644 index 000000000000..1ed185c732be --- /dev/null +++ b/test/runtime/samples/key-block-3/main.svelte @@ -0,0 +1,7 @@ + + +{#key value} +
+{/key} \ No newline at end of file From 7f3cdf6c91308b43931351458acabd2d0daf371e Mon Sep 17 00:00:00 2001 From: Conduitry Date: Fri, 25 Sep 2020 10:04:11 -0400 Subject: [PATCH 7/8] adjust docs --- site/content/docs/02-template-syntax.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/site/content/docs/02-template-syntax.md b/site/content/docs/02-template-syntax.md index 10e59e8a1f10..d955f650e20a 100644 --- a/site/content/docs/02-template-syntax.md +++ b/site/content/docs/02-template-syntax.md @@ -348,11 +348,11 @@ If you don't care about the pending state, you can also omit the initial block. {#key expression}...{/key} ``` ---- +Key blocks destroy and recreate their contents when the value of an expression changes. -Key block destroys and recreates the elements when the value of the expression changes. +--- -This is useful if you want to transition the element whenever the expression value changes. +This is useful if you want an element to play its transition whenever a value changes. ```sv {#key value} @@ -360,7 +360,9 @@ This is useful if you want to transition the element whenever the expression val {/key} ``` -Another use case is to reinitialise a Svelte component. +--- + +When used around components, this will cause them to be reinstantiated and reinitialised. ```sv {#key value} From 526995103fa658aec1bf29c002b29106b7a0e6dd Mon Sep 17 00:00:00 2001 From: Conduitry Date: Fri, 25 Sep 2020 10:05:33 -0400 Subject: [PATCH 8/8] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2734f9dcc4ec..98fe7b876cd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Svelte changelog +## Unreleased + +* Add `{#key}` block for keying arbitrary content on an expression ([#1469](https://github.com/sveltejs/svelte/issues/1469)) + ## 3.27.0 * Add `|nonpassive` event modifier, explicitly passing `passive: false` ([#2068](https://github.com/sveltejs/svelte/issues/2068))