Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

{#key} block #5397

Merged
merged 9 commits into from
Sep 25, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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"));
}
};