Skip to content

Commit 9af5c27

Browse files
authored
Merge pull request #209 from sveltejs/gh-81
Keyed updates
2 parents e5b4df8 + d0ffb64 commit 9af5c27

File tree

10 files changed

+198
-34
lines changed

10 files changed

+198
-34
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ coverage
99
coverage.lcov
1010
test/sourcemaps/*/output.js
1111
test/sourcemaps/*/output.js.map
12+
scratch

src/generators/dom/index.js

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,22 +50,30 @@ class DomGenerator extends Generator {
5050
` );
5151
}
5252

53+
const properties = new CodeBuilder();
54+
55+
if ( fragment.key ) properties.addBlock( `key: key,` );
56+
57+
properties.addBlock( deindent`
58+
mount: function ( target, anchor ) {
59+
${fragment.builders.mount}
60+
},
61+
62+
update: function ( changed, ${fragment.params} ) {
63+
${fragment.builders.update}
64+
},
65+
66+
teardown: function ( detach ) {
67+
${fragment.builders.teardown}
68+
}
69+
` );
70+
5371
this.renderers.push( deindent`
54-
function ${fragment.name} ( ${fragment.params}, component ) {
72+
function ${fragment.name} ( ${fragment.params}, component${fragment.key ? `, key` : ''} ) {
5573
${fragment.builders.init}
5674
5775
return {
58-
mount: function ( target, anchor ) {
59-
${fragment.builders.mount}
60-
},
61-
62-
update: function ( changed, ${fragment.params} ) {
63-
${fragment.builders.update}
64-
},
65-
66-
teardown: function ( detach ) {
67-
${fragment.builders.teardown}
68-
}
76+
${properties}
6977
};
7078
}
7179
` );
@@ -127,6 +135,7 @@ export default function dom ( parsed, source, options, names ) {
127135
namespace,
128136
target: 'target',
129137
localElementDepth: 0,
138+
key: null,
130139

131140
contexts: {},
132141
indexes: {},

src/generators/dom/visitors/Component.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ export default {
117117
namespace: local.namespace,
118118
target: name,
119119
parent: generator.current,
120-
localElementDepth: generator.current.localElementDepth + 1
120+
localElementDepth: generator.current.localElementDepth + 1,
121+
key: null
121122
});
122123
},
123124

src/generators/dom/visitors/EachBlock.js

Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import CodeBuilder from '../../../utils/CodeBuilder.js';
12
import deindent from '../../../utils/deindent.js';
23
import getBuilders from '../utils/getBuilders.js';
34

@@ -22,16 +23,42 @@ export default {
2223
const anchor = `${name}_anchor`;
2324
generator.createAnchor( anchor, `#each ${generator.source.slice( node.expression.start, node.expression.end )}` );
2425

25-
generator.current.builders.init.addBlock( deindent`
26-
var ${name}_value = ${snippet};
27-
var ${iterations} = [];
28-
${node.else ? `var ${elseName} = null;` : ''}
26+
generator.current.builders.init.addLine( `var ${name}_value = ${snippet};` );
27+
generator.current.builders.init.addLine( `var ${iterations} = [];` );
28+
if ( node.key ) generator.current.builders.init.addLine( `var ${name}_lookup = Object.create( null );` );
29+
if ( node.else ) generator.current.builders.init.addLine( `var ${elseName} = null;` );
30+
31+
const initialRender = new CodeBuilder();
32+
33+
const localVars = {};
34+
35+
if ( node.key ) {
36+
localVars.fragment = generator.current.getUniqueName( 'fragment' );
37+
localVars.value = generator.current.getUniqueName( 'value' );
38+
localVars.key = generator.current.getUniqueName( 'key' );
39+
40+
initialRender.addBlock( deindent`
41+
var ${localVars.key} = ${name}_value[${i}].${node.key};
42+
${name}_iterations[${i}] = ${name}_lookup[ ${localVars.key} ] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, component${node.key ? `, ${localVars.key}` : `` } );
43+
` );
44+
} else {
45+
initialRender.addLine(
46+
`${name}_iterations[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, component );`
47+
);
48+
}
2949

50+
if ( !isToplevel ) {
51+
initialRender.addLine(
52+
`${name}_iterations[${i}].mount( ${anchor}.parentNode, ${anchor} );`
53+
);
54+
}
55+
56+
generator.current.builders.init.addBlock( deindent`
3057
for ( var ${i} = 0; ${i} < ${name}_value.length; ${i} += 1 ) {
31-
${iterations}[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, component );
32-
${!isToplevel ? `${iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} );` : ''}
58+
${initialRender}
3359
}
3460
` );
61+
3562
if ( node.else ) {
3663
generator.current.builders.init.addBlock( deindent`
3764
if ( !${name}_value.length ) {
@@ -56,24 +83,62 @@ export default {
5683
}
5784
}
5885

59-
generator.current.builders.update.addBlock( deindent`
60-
var ${name}_value = ${snippet};
86+
if ( node.key ) {
87+
generator.current.builders.update.addBlock( deindent`
88+
var ${name}_value = ${snippet};
89+
var _${name}_iterations = [];
90+
var _${name}_lookup = Object.create( null );
91+
92+
var ${localVars.fragment} = document.createDocumentFragment();
93+
94+
// create new iterations as necessary
95+
for ( var ${i} = 0; ${i} < ${name}_value.length; ${i} += 1 ) {
96+
var ${localVars.value} = ${name}_value[${i}];
97+
var ${localVars.key} = ${localVars.value}.${node.key};
98+
99+
if ( ${name}_lookup[ ${localVars.key} ] ) {
100+
_${name}_iterations[${i}] = _${name}_lookup[ ${localVars.key} ] = ${name}_lookup[ ${localVars.key} ];
101+
_${name}_lookup[ ${localVars.key} ].update( changed, ${params}, ${listName}, ${listName}[${i}], ${i} );
102+
} else {
103+
_${name}_iterations[${i}] = _${name}_lookup[ ${localVars.key} ] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, component${node.key ? `, ${localVars.key}` : `` } );
104+
}
61105
62-
for ( var ${i} = 0; ${i} < ${name}_value.length; ${i} += 1 ) {
63-
if ( !${iterations}[${i}] ) {
64-
${iterations}[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, component );
65-
${iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} );
66-
} else {
67-
${iterations}[${i}].update( changed, ${params}, ${listName}, ${listName}[${i}], ${i} );
106+
_${name}_iterations[${i}].mount( ${localVars.fragment}, null );
68107
}
69-
}
70108
71-
for ( var ${i} = ${name}_value.length; ${i} < ${iterations}.length; ${i} += 1 ) {
72-
${iterations}[${i}].teardown( true );
73-
}
109+
// remove old iterations
110+
for ( var ${i} = 0; ${i} < ${name}_iterations.length; ${i} += 1 ) {
111+
var ${name}_iteration = ${name}_iterations[${i}];
112+
if ( !_${name}_lookup[ ${name}_iteration.${localVars.key} ] ) {
113+
${name}_iteration.teardown( true );
114+
}
115+
}
74116
75-
${iterations}.length = ${listName}.length;
76-
` );
117+
${name}_anchor.parentNode.insertBefore( ${localVars.fragment}, ${name}_anchor );
118+
119+
${name}_iterations = _${name}_iterations;
120+
${name}_lookup = _${name}_lookup;
121+
` );
122+
} else {
123+
generator.current.builders.update.addBlock( deindent`
124+
var ${name}_value = ${snippet};
125+
126+
for ( var ${i} = 0; ${i} < ${name}_value.length; ${i} += 1 ) {
127+
if ( !${iterations}[${i}] ) {
128+
${iterations}[${i}] = ${renderer}( ${params}, ${listName}, ${listName}[${i}], ${i}, component );
129+
${iterations}[${i}].mount( ${anchor}.parentNode, ${anchor} );
130+
} else {
131+
${iterations}[${i}].update( changed, ${params}, ${listName}, ${listName}[${i}], ${i} );
132+
}
133+
}
134+
135+
for ( var ${i} = ${name}_value.length; ${i} < ${iterations}.length; ${i} += 1 ) {
136+
${iterations}[${i}].teardown( true );
137+
}
138+
139+
${iterations}.length = ${listName}.length;
140+
` );
141+
}
77142

78143
if ( node.else ) {
79144
generator.current.builders.update.addBlock( deindent`
@@ -128,6 +193,7 @@ export default {
128193
target: 'target',
129194
expression: node.expression,
130195
context: node.context,
196+
key: node.key,
131197
localElementDepth: 0,
132198

133199
contextDependencies,

src/generators/dom/visitors/Element.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ export default {
8888
namespace: local.namespace,
8989
target: name,
9090
parent: generator.current,
91-
localElementDepth: generator.current.localElementDepth + 1
91+
localElementDepth: generator.current.localElementDepth + 1,
92+
key: null
9293
});
9394
},
9495

src/parse/state/mustache.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,12 @@ export default function mustache ( parser ) {
171171
if ( !block.index ) parser.error( `Expected name` );
172172
parser.allowWhitespace();
173173
}
174+
175+
if ( parser.eat( '@' ) ) {
176+
block.key = parser.read( validIdentifier );
177+
if ( !block.key ) parser.error( `Expected name` );
178+
parser.allowWhitespace();
179+
}
174180
}
175181

176182
parser.eat( '}}', true );
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export default {
2+
data: {
3+
todos: [
4+
{ id: 123, description: 'implement keyed each blocks' },
5+
{ id: 234, description: 'implement client-side hydration' }
6+
]
7+
},
8+
9+
html: '<p>implement keyed each blocks</p><p>implement client-side hydration</p>',
10+
11+
test ( assert, component, target ) {
12+
const [ p1, p2 ] = target.querySelectorAll( 'p' );
13+
14+
component.set({
15+
todos: [
16+
{ id: 234, description: 'implement client-side hydration' }
17+
]
18+
});
19+
assert.htmlEqual( target.innerHTML, '<p>implement client-side hydration</p>' );
20+
21+
const [ p3 ] = target.querySelectorAll( 'p' );
22+
23+
assert.ok( !target.contains( p1 ), 'first <p> element should be removed' );
24+
assert.equal( p2, p3, 'second <p> element should be retained' );
25+
26+
component.teardown();
27+
}
28+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{{#each todos as todo @id}}
2+
<p>{{todo.description}}</p>
3+
{{/each}}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{{#each todos as todo @id}}
2+
<p>{{todo}}</p>
3+
{{/each}}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"html": {
3+
"start": 0,
4+
"end": 54,
5+
"type": "Fragment",
6+
"children": [
7+
{
8+
"start": 0,
9+
"end": 54,
10+
"type": "EachBlock",
11+
"expression": {
12+
"start": 8,
13+
"end": 13,
14+
"type": "Identifier",
15+
"name": "todos"
16+
},
17+
"context": "todo",
18+
"key": "id",
19+
"children": [
20+
{
21+
"start": 29,
22+
"end": 44,
23+
"type": "Element",
24+
"name": "p",
25+
"attributes": [],
26+
"children": [
27+
{
28+
"start": 32,
29+
"end": 40,
30+
"type": "MustacheTag",
31+
"expression": {
32+
"start": 34,
33+
"end": 38,
34+
"type": "Identifier",
35+
"name": "todo"
36+
}
37+
}
38+
]
39+
}
40+
]
41+
}
42+
]
43+
},
44+
"css": null,
45+
"js": null
46+
}

0 commit comments

Comments
 (0)