Skip to content

Commit

Permalink
{#key} block (#5397)
Browse files Browse the repository at this point in the history
Co-authored-by: Conduitry <git@chor.date>
  • Loading branch information
tanhauhau and Conduitry committed Sep 25, 2020
1 parent 0ca1dcd commit fa7c780
Show file tree
Hide file tree
Showing 28 changed files with 459 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
27 changes: 27 additions & 0 deletions site/content/docs/02-template-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,33 @@ If you don't care about the pending state, you can also omit the initial block.
{/await}
```

### {#key ...}

```sv
{#key expression}...{/key}
```

Key blocks destroy and recreate their contents when the value of an expression changes.

---

This is useful if you want an element to play its transition whenever a value changes.

```sv
{#key value}
<div transition:fade>{value}</div>
{/key}
```

---

When used around components, this will cause them to be reinstantiated and reinitialised.

```sv
{#key value}
<Component />
{/key}
```

### {@html ...}

Expand Down
19 changes: 19 additions & 0 deletions src/compiler/compile/nodes/KeyBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Expression from "./shared/Expression";
import map_children from "./shared/map_children";
import AbstractBlock from "./shared/AbstractBlock";

export default class KeyBlock extends AbstractBlock {
type: "KeyBlock";

expression: Expression;

constructor(component, parent, scope, info) {
super(component, parent, scope, info);

this.expression = new Expression(component, this, scope, info.expression);

this.children = map_children(component, this, scope, info.children);

this.warn_if_empty_block();
}
}
2 changes: 2 additions & 0 deletions src/compiler/compile/nodes/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -50,6 +51,7 @@ export type INode = Action
| Head
| IfBlock
| InlineComponent
| KeyBlock
| Let
| MustacheTag
| Options
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/compile/nodes/shared/map_children.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions src/compiler/compile/render_dom/wrappers/Fragment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -30,6 +31,7 @@ const wrappers = {
Head,
IfBlock,
InlineComponent,
KeyBlock,
MustacheTag,
Options: null,
RawMustacheTag,
Expand Down
136 changes: 136 additions & 0 deletions src/compiler/compile/render_dom/wrappers/KeyBlock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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();

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,
node.children,
parent,
strip_whitespace,
next_sibling
);
}

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,
(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_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);
`);
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)`);
}
}
2 changes: 2 additions & 0 deletions src/compiler/compile/render_ssr/Renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -30,6 +31,7 @@ const handlers: Record<string, Handler> = {
Head,
IfBlock,
InlineComponent,
KeyBlock,
MustacheTag: Tag, // TODO MustacheTag is an anachronism
Options: noop,
RawMustacheTag: HtmlTag,
Expand Down
6 changes: 6 additions & 0 deletions src/compiler/compile/render_ssr/handlers/KeyBlock.ts
Original file line number Diff line number Diff line change
@@ -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);
}
8 changes: 6 additions & 2 deletions src/compiler/parse/state/mustache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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`,
Expand Down Expand Up @@ -221,10 +223,12 @@ 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`,
message: `Expected if, each or await`
message: `Expected if, each, await or key`
});
}

Expand Down
14 changes: 14 additions & 0 deletions test/runtime/samples/key-block-2/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// with reactive content beside `key`
export default {
html: `<div>00</div>`,
async test({ assert, component, target, window }) {
const div = target.querySelector('div');
component.reactive = 2;
assert.htmlEqual(target.innerHTML, `<div>02</div>`);
assert.strictEqual(div, target.querySelector('div'));

component.value = 5;
assert.htmlEqual(target.innerHTML, `<div>52</div>`);
assert.notStrictEqual(div, target.querySelector('div'));
}
};
8 changes: 8 additions & 0 deletions test/runtime/samples/key-block-2/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script>
export let value = 0;
export let reactive = 0;
</script>

{#key value}
<div>{value}{reactive}</div>
{/key}
11 changes: 11 additions & 0 deletions test/runtime/samples/key-block-3/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// key is not used in the template
export default {
html: `<div></div>`,
async test({ assert, component, target, window }) {
const div = target.querySelector('div');

component.value = 5;
assert.htmlEqual(target.innerHTML, `<div></div>`);
assert.notStrictEqual(div, target.querySelector('div'));
}
};
7 changes: 7 additions & 0 deletions test/runtime/samples/key-block-3/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
export let value = 0;
</script>

{#key value}
<div />
{/key}
15 changes: 15 additions & 0 deletions test/runtime/samples/key-block-array-immutable/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default {
html: `<div>1</div>`,
async test({ assert, component, target, window }) {
let div = target.querySelector("div");
await component.append(2);
assert.htmlEqual(target.innerHTML, `<div>1</div>`);
assert.strictEqual(div, target.querySelector("div"));

div = target.querySelector("div");

component.array = [3, 4];
assert.htmlEqual(target.innerHTML, `<div>3,4</div>`);
assert.notStrictEqual(div, target.querySelector("div"));
}
};
14 changes: 14 additions & 0 deletions test/runtime/samples/key-block-array-immutable/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<svelte:options immutable />

<script>
export let array = [1];
export function append(value) {
array.push(value);
array = array;
}
</script>

{#key array}
<div>{array.join(',')}</div>
{/key}
15 changes: 15 additions & 0 deletions test/runtime/samples/key-block-array/_config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export default {
html: `<div>1</div>`,
async test({ assert, component, target, window }) {
let div = target.querySelector("div");
await component.append(2);
assert.htmlEqual(target.innerHTML, `<div>1,2</div>`);
assert.notStrictEqual(div, target.querySelector("div"));

div = target.querySelector("div");

component.array = [3, 4];
assert.htmlEqual(target.innerHTML, `<div>3,4</div>`);
assert.notStrictEqual(div, target.querySelector("div"));
}
};

0 comments on commit fa7c780

Please sign in to comment.