Skip to content

Commit fa7c780

Browse files
tanhauhauConduitry
andauthored
{#key} block (#5397)
Co-authored-by: Conduitry <[email protected]>
1 parent 0ca1dcd commit fa7c780

File tree

28 files changed

+459
-2
lines changed

28 files changed

+459
-2
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Svelte changelog
22

3+
## Unreleased
4+
5+
* Add `{#key}` block for keying arbitrary content on an expression ([#1469](https://github.com/sveltejs/svelte/issues/1469))
6+
37
## 3.27.0
48

59
* Add `|nonpassive` event modifier, explicitly passing `passive: false` ([#2068](https://github.com/sveltejs/svelte/issues/2068))

site/content/docs/02-template-syntax.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,33 @@ If you don't care about the pending state, you can also omit the initial block.
342342
{/await}
343343
```
344344

345+
### {#key ...}
346+
347+
```sv
348+
{#key expression}...{/key}
349+
```
350+
351+
Key blocks destroy and recreate their contents when the value of an expression changes.
352+
353+
---
354+
355+
This is useful if you want an element to play its transition whenever a value changes.
356+
357+
```sv
358+
{#key value}
359+
<div transition:fade>{value}</div>
360+
{/key}
361+
```
362+
363+
---
364+
365+
When used around components, this will cause them to be reinstantiated and reinitialised.
366+
367+
```sv
368+
{#key value}
369+
<Component />
370+
{/key}
371+
```
345372

346373
### {@html ...}
347374

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Expression from "./shared/Expression";
2+
import map_children from "./shared/map_children";
3+
import AbstractBlock from "./shared/AbstractBlock";
4+
5+
export default class KeyBlock extends AbstractBlock {
6+
type: "KeyBlock";
7+
8+
expression: Expression;
9+
10+
constructor(component, parent, scope, info) {
11+
super(component, parent, scope, info);
12+
13+
this.expression = new Expression(component, this, scope, info.expression);
14+
15+
this.children = map_children(component, this, scope, info.children);
16+
17+
this.warn_if_empty_block();
18+
}
19+
}

src/compiler/compile/nodes/interfaces.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import Fragment from './Fragment';
1818
import Head from './Head';
1919
import IfBlock from './IfBlock';
2020
import InlineComponent from './InlineComponent';
21+
import KeyBlock from './KeyBlock';
2122
import Let from './Let';
2223
import MustacheTag from './MustacheTag';
2324
import Options from './Options';
@@ -50,6 +51,7 @@ export type INode = Action
5051
| Head
5152
| IfBlock
5253
| InlineComponent
54+
| KeyBlock
5355
| Let
5456
| MustacheTag
5557
| Options

src/compiler/compile/nodes/shared/map_children.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Element from '../Element';
66
import Head from '../Head';
77
import IfBlock from '../IfBlock';
88
import InlineComponent from '../InlineComponent';
9+
import KeyBlock from '../KeyBlock';
910
import MustacheTag from '../MustacheTag';
1011
import Options from '../Options';
1112
import RawMustacheTag from '../RawMustacheTag';
@@ -28,6 +29,7 @@ function get_constructor(type) {
2829
case 'Head': return Head;
2930
case 'IfBlock': return IfBlock;
3031
case 'InlineComponent': return InlineComponent;
32+
case 'KeyBlock': return KeyBlock;
3133
case 'MustacheTag': return MustacheTag;
3234
case 'Options': return Options;
3335
case 'RawMustacheTag': return RawMustacheTag;

src/compiler/compile/render_dom/wrappers/Fragment.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import EachBlock from './EachBlock';
66
import Element from './Element/index';
77
import Head from './Head';
88
import IfBlock from './IfBlock';
9+
import KeyBlock from './KeyBlock';
910
import InlineComponent from './InlineComponent/index';
1011
import MustacheTag from './MustacheTag';
1112
import RawMustacheTag from './RawMustacheTag';
@@ -30,6 +31,7 @@ const wrappers = {
3031
Head,
3132
IfBlock,
3233
InlineComponent,
34+
KeyBlock,
3335
MustacheTag,
3436
Options: null,
3537
RawMustacheTag,
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
import Wrapper from "./shared/Wrapper";
2+
import Renderer from "../Renderer";
3+
import Block from "../Block";
4+
import EachBlock from "../../nodes/EachBlock";
5+
import KeyBlock from "../../nodes/KeyBlock";
6+
import create_debugging_comment from "./shared/create_debugging_comment";
7+
import FragmentWrapper from "./Fragment";
8+
import { b, x } from "code-red";
9+
import { Identifier } from "estree";
10+
11+
export default class KeyBlockWrapper extends Wrapper {
12+
node: KeyBlock;
13+
fragment: FragmentWrapper;
14+
block: Block;
15+
dependencies: string[];
16+
var: Identifier = { type: "Identifier", name: "key_block" };
17+
18+
constructor(
19+
renderer: Renderer,
20+
block: Block,
21+
parent: Wrapper,
22+
node: EachBlock,
23+
strip_whitespace: boolean,
24+
next_sibling: Wrapper
25+
) {
26+
super(renderer, block, parent, node);
27+
28+
this.cannot_use_innerhtml();
29+
this.not_static_content();
30+
31+
this.dependencies = node.expression.dynamic_dependencies();
32+
33+
if (this.dependencies.length) {
34+
block = block.child({
35+
comment: create_debugging_comment(node, renderer.component),
36+
name: renderer.component.get_unique_name("create_key_block"),
37+
type: "key"
38+
});
39+
renderer.blocks.push(block);
40+
}
41+
42+
this.block = block;
43+
this.fragment = new FragmentWrapper(
44+
renderer,
45+
this.block,
46+
node.children,
47+
parent,
48+
strip_whitespace,
49+
next_sibling
50+
);
51+
}
52+
53+
render(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
54+
if (this.dependencies.length === 0) {
55+
this.render_static_key(block, parent_node, parent_nodes);
56+
} else {
57+
this.render_dynamic_key(block, parent_node, parent_nodes);
58+
}
59+
}
60+
61+
render_static_key(_block: Block, parent_node: Identifier, parent_nodes: Identifier) {
62+
this.fragment.render(this.block, parent_node, parent_nodes);
63+
}
64+
65+
render_dynamic_key(block: Block, parent_node: Identifier, parent_nodes: Identifier) {
66+
this.fragment.render(
67+
this.block,
68+
null,
69+
(x`#nodes` as unknown) as Identifier
70+
);
71+
72+
const has_transitions = !!(
73+
this.block.has_intro_method || this.block.has_outro_method
74+
);
75+
const dynamic = this.block.has_update_method;
76+
77+
const previous_key = block.get_unique_name('previous_key');
78+
const snippet = this.node.expression.manipulate(block);
79+
block.add_variable(previous_key, snippet);
80+
81+
const not_equal = this.renderer.component.component_options.immutable ? x`@not_equal` : x`@safe_not_equal`;
82+
const condition = x`${this.renderer.dirty(this.dependencies)} && ${not_equal}(${previous_key}, ${previous_key} = ${snippet})`;
83+
84+
block.chunks.init.push(b`
85+
let ${this.var} = ${this.block.name}(#ctx);
86+
`);
87+
block.chunks.create.push(b`${this.var}.c();`);
88+
if (this.renderer.options.hydratable) {
89+
block.chunks.claim.push(b`${this.var}.l(${parent_nodes});`);
90+
}
91+
block.chunks.mount.push(
92+
b`${this.var}.m(${parent_node || "#target"}, ${
93+
parent_node ? "null" : "#anchor"
94+
});`
95+
);
96+
const anchor = this.get_or_create_anchor(block, parent_node, parent_nodes);
97+
const body = b`
98+
${
99+
has_transitions
100+
? b`
101+
@group_outros();
102+
@transition_out(${this.var}, 1, 1, @noop);
103+
@check_outros();
104+
`
105+
: b`${this.var}.d(1);`
106+
}
107+
${this.var} = ${this.block.name}(#ctx);
108+
${this.var}.c();
109+
${has_transitions && b`@transition_in(${this.var})`}
110+
${this.var}.m(${this.get_update_mount_node(anchor)}, ${anchor});
111+
`;
112+
113+
if (dynamic) {
114+
block.chunks.update.push(b`
115+
if (${condition}) {
116+
${body}
117+
} else {
118+
${this.var}.p(#ctx, #dirty);
119+
}
120+
`);
121+
} else {
122+
block.chunks.update.push(b`
123+
if (${condition}) {
124+
${body}
125+
}
126+
`);
127+
}
128+
129+
if (has_transitions) {
130+
block.chunks.intro.push(b`@transition_in(${this.var})`);
131+
block.chunks.outro.push(b`@transition_out(${this.var})`);
132+
}
133+
134+
block.chunks.destroy.push(b`${this.var}.d(detaching)`);
135+
}
136+
}

src/compiler/compile/render_ssr/Renderer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Head from './handlers/Head';
77
import HtmlTag from './handlers/HtmlTag';
88
import IfBlock from './handlers/IfBlock';
99
import InlineComponent from './handlers/InlineComponent';
10+
import KeyBlock from './handlers/KeyBlock';
1011
import Slot from './handlers/Slot';
1112
import Tag from './handlers/Tag';
1213
import Text from './handlers/Text';
@@ -30,6 +31,7 @@ const handlers: Record<string, Handler> = {
3031
Head,
3132
IfBlock,
3233
InlineComponent,
34+
KeyBlock,
3335
MustacheTag: Tag, // TODO MustacheTag is an anachronism
3436
Options: noop,
3537
RawMustacheTag: HtmlTag,
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import KeyBlock from '../../nodes/KeyBlock';
2+
import Renderer, { RenderOptions } from '../Renderer';
3+
4+
export default function(node: KeyBlock, renderer: Renderer, options: RenderOptions) {
5+
renderer.render(node.children, options);
6+
}

src/compiler/parse/state/mustache.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export default function mustache(parser: Parser) {
3838

3939
parser.allow_whitespace();
4040

41-
// {/if}, {/each} or {/await}
41+
// {/if}, {/each}, {/await} or {/key}
4242
if (parser.eat('/')) {
4343
let block = parser.current();
4444
let expected;
@@ -63,6 +63,8 @@ export default function mustache(parser: Parser) {
6363
expected = 'each';
6464
} else if (block.type === 'AwaitBlock') {
6565
expected = 'await';
66+
} else if (block.type === 'KeyBlock') {
67+
expected = 'key';
6668
} else {
6769
parser.error({
6870
code: `unexpected-block-close`,
@@ -221,10 +223,12 @@ export default function mustache(parser: Parser) {
221223
type = 'EachBlock';
222224
} else if (parser.eat('await')) {
223225
type = 'AwaitBlock';
226+
} else if (parser.eat('key')) {
227+
type = 'KeyBlock';
224228
} else {
225229
parser.error({
226230
code: `expected-block-type`,
227-
message: `Expected if, each or await`
231+
message: `Expected if, each, await or key`
228232
});
229233
}
230234

0 commit comments

Comments
 (0)