Repository: sveltejs/svelte (JavaScript) — Feature

Issue:
{{#each things as thing by thing}}
As mentioned in #700, it's sometimes useful to be able to use a list item as the basis for keyed updates, rather than a property of each list item.

That could be done a few different ways, e.g.:

```html
{{#each things as thing @}}
{{#each things as thing @this}}
{{#each things as thing by thing}}
```

One potentially nice feature of the last option, apart from the fact that it doesn't have off-putting special characters, is that it could be more flexible — as well as `by thing` or `by thing.id` it could potentially be *any* expression:

```html
{{#each things as thing by getId(thing)}}
```

Haven't looked into whether that's feasible. Just wondering about whether it would be desirable if so. Thoughts?


Discussion:
I actually really hate this in AngularJS. I can never get it right, and the documentation is very unclear.

The only support I'd want to see is the ability to iterate Objects by key, which could be done in a simple, javascripty way:

```
{{ #each things as key, value }}
```
It seems #704 canonizes the `@property` syntax, so I'd favor sticking close to that, so I don't have to remember the difference between `@` and `by`.

Since `this` is a valid property name, `{{#each things as thing @}}` seems best to me?
I like the idea of this as I could imagine reusing a key function that gets passed around to various places
I've got a few thoughts about each of the current proposals:

`{{#each things as thing @}}`

Looks like you forgot to type a value after the `@`

`{{#each things as thing @this}}`

`this` has its own baggage and seems likely to cause confusion.

`{{#each things as thing by thing}}`

Keyed updates are already using the `@<key>` syntax, so this seems like a non-starter.

For this particular use-case I'd expect something like

`{{#each things as thing @thing}}`

Which clearly illustrates the intent of using `thing` itself as the key. It requires some repetition but the clarity is worth it over the terseness of just using `@`.
I'd like to add that, if there's any doubt as to the desirability of this feature, it'd be really useful for generic reusable components which act upon arrays of varying objects. For instance, user code might provide an array of objects of any kind to a reusable component, and could specify the unique key, or a method to generate the unique key, for each object, for the reusable component to use.

On the matter of syntax, I feel that `by` with an expression is most appropriate. To start with, it's most JavaScript-like, which seems appropriate given we work in the context of JavaScript and Svelte has mostly been built with this in mind. Second, it follows logically from the rest of the `{{each}}` block, which already uses another keyword, `as`. And finally, it carries more semantic meaning, by virtue of it being a word. Even if `@` is understood as 'at', it makes more sense to say that an `{{each}}` block is keyed _by_ a key, not _at_ a key. In contrast, the current `@` syntax lacks these advantages, which make it seem out of place. The strength of `by`, as an expression, can be demonstrated by a number of examples.

```JavaScript
{{#each things as thing by 'key'}}
{{#each things as thing by keyProperty}}
{{#each things as thing by options.key}}
{{#each things as thing by thing.getKey()}}
{{#each things as thing by getKey(thing)}}
```

I don't think the existence of the current `@` syntax should be an impediment to shifting to `by` from this point onwards. It should be possible to continue to allow `@` syntax in its current form for the foreseeable future for backwards compatibility, and potentially eventually deprecate it. It also doesn't seem like the `@` syntax easily lends itself to supporting an expression. The text to the right of the `@` is currently treated as the key itself, and so an expression would need to be denoted by _further_ special symbols, like  `@<expressionReturningKey>`.

I can't comment on the feasibility, but I hope it's not problematic.

So, I think there's pretty good reason, both for this feature as a concept, and for its implementation with the `by` expression syntax in particular.
Something related here but not mentioned is that we will need to have keyed each block support using an array index too.

For larger lists I always get the value from the server the fastest way possible as a json array of array of values only (rather than an array of objects with the keys being repeated for every single row). 

```
{{#each rs as o @o[1]}} 
or (my vote in this situation)
{{#each rs as o @1}} 
```
I assume that'd be supported by the `by` construct:
```
{{#each things as thing by thing[1]}}
```
This seems like the most flexible approach-- and I like the fact that it doesn't use sigils.
Seems like this was closed by #1318?

https://svelte.technology/guide#keyed-each-blocks

```html
{#each things as thing (thing.key)}
```
The syntax has changed, but what's actually permitted as a keyed index has not. IMO this should not be closed until arbitrary expressions are allowed as keys.
Not quite — the syntax is there, but it doesn't currently support any form other than `(x.y)`
dammit @Conduitry. yes, exactly that
So given that the syntax should be `{#each things as thing (thing)}` and now it needs to be implemented?

Resolving patch:
diff --git a/src/compile/nodes/EachBlock.ts b/src/compile/nodes/EachBlock.ts
--- a/src/compile/nodes/EachBlock.ts
+++ b/src/compile/nodes/EachBlock.ts
@@ -16,7 +16,7 @@ export default class EachBlock extends Node {
 	iterations: string;
 	index: string;
 	context: string;
-	key: string;
+	key: Expression;
 	scope: TemplateScope;
 	destructuredContexts: string[];
 
@@ -29,7 +29,10 @@ export default class EachBlock extends Node {
 		this.expression = new Expression(compiler, this, scope, info.expression);
 		this.context = info.context;
 		this.index = info.index;
-		this.key = info.key;
+
+		this.key = info.key
+			? new Expression(compiler, this, scope, info.key)
+			: null;
 
 		this.scope = scope.child();
 
@@ -262,7 +265,7 @@ export default class EachBlock extends Node {
 			mountOrIntro,
 		}
 	) {
-		const key = block.getUniqueName('key');
+		const get_key = block.getUniqueName('get_key');
 		const blocks = block.getUniqueName(`${each}_blocks`);
 		const lookup = block.getUniqueName(`${each}_lookup`);
 
@@ -282,11 +285,14 @@ export default class EachBlock extends Node {
 		}
 
 		block.builders.init.addBlock(deindent`
+			const ${get_key} = ctx => ${this.key.snippet};
+
 			for (var #i = 0; #i < ${each_block_value}.${length}; #i += 1) {
-				var ${key} = ${each_block_value}[#i].${this.key};
-				${blocks}[#i] = ${lookup}[${key}] = ${create_each_block}(#component, ${key}, @assign(@assign({}, ctx), {
+				let child_ctx = @assign(@assign({}, ctx), {
 					${this.contextProps.join(',\n')}
-				}));
+				});
+				let key = ${get_key}(child_ctx);
+				${blocks}[#i] = ${lookup}[key] = ${create_each_block}(#component, key, child_ctx);
 			}
 		`);
 
@@ -313,7 +319,7 @@ export default class EachBlock extends Node {
 		block.builders.update.addBlock(deindent`
 			var ${each_block_value} = ${snippet};
 
-			${blocks} = @updateKeyedEach(${blocks}, #component, changed, "${this.key}", ${dynamic ? '1' : '0'}, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, function(#i) {
+			${blocks} = @updateKeyedEach(${blocks}, #component, changed, ${get_key}, ${dynamic ? '1' : '0'}, ${each_block_value}, ${lookup}, ${updateMountNode}, ${String(this.block.hasOutroMethod)}, ${create_each_block}, "${mountOrIntro}", ${anchor}, function(#i) {
 				return @assign(@assign({}, ctx), {
 					${this.contextProps.join(',\n')}
 				});
diff --git a/src/parse/state/mustache.ts b/src/parse/state/mustache.ts
--- a/src/parse/state/mustache.ts
+++ b/src/parse/state/mustache.ts
@@ -299,23 +299,7 @@ export default function mustache(parser: Parser) {
 			if (parser.eat('(')) {
 				parser.allowWhitespace();
 
-				const expression = readExpression(parser);
-
-				// TODO eventually, we should accept any expression, and turn
-				// it into a function. For now, assume that every expression
-				// follows the `foo.id` pattern, and equates to `@id`
-				if (
-					expression.type !== 'MemberExpression' ||
-					expression.property.computed ||
-					expression.property.type !== 'Identifier'
-				) {
-					parser.error({
-						code: `invalid-key`,
-						message: 'invalid key'
-					}, expression.start);
-				}
-
-				block.key = expression.property.name;
+				block.key = readExpression(parser);
 				parser.allowWhitespace();
 				parser.eat(')', true);
 				parser.allowWhitespace();
diff --git a/src/shared/keyed-each.js b/src/shared/keyed-each.js
--- a/src/shared/keyed-each.js
+++ b/src/shared/keyed-each.js
@@ -10,7 +10,7 @@ export function outroAndDestroyBlock(block, lookup) {
 	});
 }
 
-export function updateKeyedEach(old_blocks, component, changed, key_prop, dynamic, list, lookup, node, has_outro, create_each_block, intro_method, next, get_context) {
+export function updateKeyedEach(old_blocks, component, changed, get_key, dynamic, list, lookup, node, has_outro, create_each_block, intro_method, next, get_context) {
 	var o = old_blocks.length;
 	var n = list.length;
 
@@ -24,14 +24,15 @@ export function updateKeyedEach(old_blocks, component, changed, key_prop, dynami
 
 	var i = n;
 	while (i--) {
-		var key = list[i][key_prop];
+		var ctx = get_context(i);
+		var key = get_key(ctx);
 		var block = lookup[key];
 
 		if (!block) {
-			block = create_each_block(component, key, get_context(i));
+			block = create_each_block(component, key, ctx);
 			block.c();
 		} else if (dynamic) {
-			block.p(changed, get_context(i));
+			block.p(changed, ctx);
 		}
 
 		new_blocks[i] = new_lookup[key] = block;


Test patch:
diff --git a/test/parser/samples/each-block-keyed/output.json b/test/parser/samples/each-block-keyed/output.json
--- a/test/parser/samples/each-block-keyed/output.json
+++ b/test/parser/samples/each-block-keyed/output.json
@@ -1,5 +1,4 @@
 {
-	"hash": "1x6az5m",
 	"html": {
 		"start": 0,
 		"end": 54,
@@ -38,7 +37,24 @@
 					}
 				],
 				"context": "todo",
-				"key": "id"
+				"key": {
+					"type": "MemberExpression",
+					"start": 22,
+					"end": 29,
+					"object": {
+						"type": "Identifier",
+						"start": 22,
+						"end": 26,
+						"name": "todo"
+					},
+					"property": {
+						"type": "Identifier",
+						"start": 27,
+						"end": 29,
+						"name": "id"
+					},
+					"computed": false
+				}
 			}
 		]
 	},
diff --git a/test/runtime/samples/each-block-keyed-non-prop/_config.js b/test/runtime/samples/each-block-keyed-non-prop/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-keyed-non-prop/_config.js
@@ -0,0 +1,31 @@
+export default {
+	data: {
+		words: ['foo', 'bar', 'baz']
+	},
+
+	html: `
+		<p>foo</p>
+		<p>bar</p>
+		<p>baz</p>
+	`,
+
+	test(assert, component, target) {
+		const [p1, p2, p3] = target.querySelectorAll('p');
+
+		component.set({
+			words: ['foo', 'baz'],
+		});
+
+		assert.htmlEqual(target.innerHTML, `
+			<p>foo</p>
+			<p>baz</p>
+		`);
+
+		const [p4, p5] = target.querySelectorAll('p');
+
+		assert.ok(!target.contains(p2), '<p> element should be removed');
+
+		assert.equal(p1, p4, 'first <p> element should be retained');
+		assert.equal(p3, p5, 'last <p> element should be retained');
+	},
+};
diff --git a/test/runtime/samples/each-block-keyed-non-prop/main.html b/test/runtime/samples/each-block-keyed-non-prop/main.html
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/each-block-keyed-non-prop/main.html
@@ -0,0 +1,3 @@
+{#each words as word (word)}
+	<p>{word}</p>
+{/each}