Repository: sveltejs/svelte (JavaScript)
Date: 2020-10-28 16:09:34+00:00
Task type: Bug Fix

Issue:
Invalid code generated when a slot prop in an #each is a function that depends on the #each context.
**Describe the bug**

Code such as this:
```svelte
<script>
    function setKey(key, value) { ... }
</script>

{#each keys as key}
    <slot {key} set={(value) => setKey(key, value)} />
{/each}
```

Generates this code: 
```js
const get_default_slot_context = ctx => ({ key: /*key*/ ctx[4], set: func });

function create_each_block(ctx) {
	let current;

	function func(...args) {
		return /*func*/ ctx[3](/*key*/ ctx[4], ...args);
	}

	const default_slot_template = /*#slots*/ ctx[2].default;
	const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[1], get_default_slot_context);

    // rest of the function here
}
```

`func` is defined inside create_each_block, not in `get_default_slot_context`, which causes a runtime error `func is not defined`.

This only happens if the value of `set` can't be hoisted outside the `#each`. If I don't use an #each loop or if I just remove the reliance on `key` like this `set={(value) => setSomething(value)}`, then everything works fine.

Looking back, 3.15 is the latest version of Svelte in which this worked.

**To Reproduce**

REPL: https://svelte.dev/repl/ad8e6f39cd20403dacd1be84d71e498d?version=3.29.3

**Information about your Svelte project:**

Svelte 3.29.3 in REPL.

**Severity**

Mildly inconvenient. I can work around it in this case by just using a static function for the slot prop and having the slot content just call the function with all the necessary arguments.



Issue discussion:

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,7 @@
 ## Unreleased
 
 * Fix `$$props` and `$$restProps` when compiling to a custom element ([#5482](https://github.com/sveltejs/svelte/issues/5482))
+* Fix function calls in `<slot>` props that use contextual values ([#5565](https://github.com/sveltejs/svelte/issues/5565))
 * Add `Element` and `Node` to known globals ([#5586](https://github.com/sveltejs/svelte/issues/5586))
 
 ## 3.29.4
diff --git a/src/compiler/compile/nodes/shared/Expression.ts b/src/compiler/compile/nodes/shared/Expression.ts
--- a/src/compiler/compile/nodes/shared/Expression.ts
+++ b/src/compiler/compile/nodes/shared/Expression.ts
@@ -4,7 +4,6 @@ import is_reference from 'is-reference';
 import flatten_reference from '../../utils/flatten_reference';
 import { create_scopes, Scope, extract_names } from '../../utils/scope';
 import { sanitize } from '../../../utils/names';
-import Wrapper from '../../render_dom/wrappers/shared/Wrapper';
 import TemplateScope from './TemplateScope';
 import get_object from '../../utils/get_object';
 import Block from '../../render_dom/Block';
@@ -12,12 +11,12 @@ import is_dynamic from '../../render_dom/wrappers/shared/is_dynamic';
 import { b } from 'code-red';
 import { invalidate } from '../../render_dom/invalidate';
 import { Node, FunctionExpression, Identifier } from 'estree';
-import { TemplateNode } from '../../../interfaces';
+import { INode } from '../interfaces';
 import { is_reserved_keyword } from '../../utils/reserved_keywords';
 import replace_object from '../../utils/replace_object';
 import EachBlock from '../EachBlock';
 
-type Owner = Wrapper | TemplateNode;
+type Owner = INode;
 
 export default class Expression {
 	type: 'Expression' = 'Expression';
@@ -37,7 +36,6 @@ export default class Expression {
 
 	manipulated: Node;
 
-	// todo: owner type
 	constructor(component: Component, owner: Owner, template_scope: TemplateScope, info, lazy?: boolean) {
 		// TODO revert to direct property access in prod?
 		Object.defineProperties(this, {
@@ -276,10 +274,12 @@ export default class Expression {
 					else {
 						// we need a combo block/init recipe
 						const deps = Array.from(contextual_dependencies);
+						const function_expression = node as FunctionExpression;
 
-						(node as FunctionExpression).params = [
+						const has_args = function_expression.params.length > 0;
+						function_expression.params = [
 							...deps.map(name => ({ type: 'Identifier', name } as Identifier)),
-							...(node as FunctionExpression).params
+							...function_expression.params
 						];
 
 						const context_args = deps.map(name => block.renderer.reference(name));
@@ -291,18 +291,49 @@ export default class Expression {
 
 						this.replace(id as any);
 
-						if ((node as FunctionExpression).params.length > 0) {
-							declarations.push(b`
-								function ${id}(...args) {
-									return ${callee}(${context_args}, ...args);
-								}
-							`);
+						const func_declaration = has_args
+							? b`function ${id}(...args) {
+								return ${callee}(${context_args}, ...args);
+							}`
+							: b`function ${id}() {
+								return ${callee}(${context_args});
+							}`;
+
+						if (owner.type === 'Attribute' && owner.parent.name === 'slot') {
+							const dep_scopes = new Set<INode>(deps.map(name => template_scope.get_owner(name)));
+							// find the nearest scopes
+							let node: INode = owner.parent;
+							while (node && !dep_scopes.has(node)) {
+								node = node.parent;
+							}
+
+							const func_expression = func_declaration[0];
+
+							if (node.type === 'InlineComponent') {
+								// <Comp let:data />
+								this.replace(func_expression);
+							} else {
+								// {#each}, {#await}
+								const func_id = component.get_unique_name(id.name + '_func');
+								block.renderer.add_to_context(func_id.name, true);
+								// rename #ctx -> child_ctx;
+								walk(func_expression, {
+									enter(node) {
+										if (node.type === 'Identifier' && node.name === '#ctx') {
+											node.name = 'child_ctx';
+										}
+									}
+								});
+								// add to get_xxx_context
+								// child_ctx[x] = function () { ... }
+								(template_scope.get_owner(deps[0]) as EachBlock).contexts.push({
+									key: func_id,
+									modifier: () => func_expression
+								});
+								this.replace(block.renderer.reference(func_id));
+							}
 						} else {
-							declarations.push(b`
-								function ${id}() {
-									return ${callee}(${context_args});
-								}
-							`);
+							declarations.push(func_declaration);
 						}
 					}
 
diff --git a/src/compiler/compile/render_dom/wrappers/EachBlock.ts b/src/compiler/compile/render_dom/wrappers/EachBlock.ts
--- a/src/compiler/compile/render_dom/wrappers/EachBlock.ts
+++ b/src/compiler/compile/render_dom/wrappers/EachBlock.ts
@@ -196,11 +196,6 @@ export default class EachBlockWrapper extends Wrapper {
 			? !this.next.is_dom_node() :
 			!parent_node || !this.parent.is_dom_node();
 
-		this.context_props = this.node.contexts.map(prop => b`child_ctx[${renderer.context_lookup.get(prop.key.name).index}] = ${prop.modifier(x`list[i]`)};`);
-
-		if (this.node.has_binding) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`);
-		if (this.node.has_binding || this.node.has_index_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`);
-
 		const snippet = this.node.expression.manipulate(block);
 
 		block.chunks.init.push(b`let ${this.vars.each_block_value} = ${snippet};`);
@@ -208,15 +203,6 @@ export default class EachBlockWrapper extends Wrapper {
 			block.chunks.init.push(b`@validate_each_argument(${this.vars.each_block_value});`);
 		}
 
-		// TODO which is better — Object.create(array) or array.slice()?
-		renderer.blocks.push(b`
-			function ${this.vars.get_each_context}(#ctx, list, i) {
-				const child_ctx = #ctx.slice();
-				${this.context_props}
-				return child_ctx;
-			}
-		`);
-
 		const initial_anchor_node: Identifier = { type: 'Identifier', name: parent_node ? 'null' : '#anchor' };
 		const initial_mount_node: Identifier = parent_node || { type: 'Identifier', name: '#target' };
 		const update_anchor_node = needs_anchor
@@ -360,6 +346,19 @@ export default class EachBlockWrapper extends Wrapper {
 		if (this.else) {
 			this.else.fragment.render(this.else.block, null, x`#nodes` as Identifier);
 		}
+
+		this.context_props = this.node.contexts.map(prop => b`child_ctx[${renderer.context_lookup.get(prop.key.name).index}] = ${prop.modifier(x`list[i]`)};`);
+
+		if (this.node.has_binding) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.vars.each_block_value.name).index}] = list;`);
+		if (this.node.has_binding || this.node.has_index_binding || this.node.index) this.context_props.push(b`child_ctx[${renderer.context_lookup.get(this.index_name.name).index}] = i;`);
+		// TODO which is better — Object.create(array) or array.slice()?
+		renderer.blocks.push(b`
+			function ${this.vars.get_each_context}(#ctx, list, i) {
+				const child_ctx = #ctx.slice();
+				${this.context_props}
+				return child_ctx;
+			}
+		`);
 	}
 
 	render_keyed({


Test patch:
diff --git a/test/runtime/samples/component-slot-context-props-each-nested/Nested.svelte b/test/runtime/samples/component-slot-context-props-each-nested/Nested.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-context-props-each-nested/Nested.svelte
@@ -0,0 +1,14 @@
+<script>
+	let keys = ['a', 'b'];
+	let items = ['c', 'd'];
+  export let log;
+	function setKey(key, value, item) {
+		log.push(`setKey(${key}, ${value}, ${item})`);
+  }
+</script>
+
+{#each items as item (item)}
+	{#each keys as key (key)}
+		<slot {key} {item} set={(value) => setKey(key, value, item)} />
+	{/each}
+{/each}
\ No newline at end of file
diff --git a/test/runtime/samples/component-slot-context-props-each-nested/_config.js b/test/runtime/samples/component-slot-context-props-each-nested/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-context-props-each-nested/_config.js
@@ -0,0 +1,36 @@
+export default {
+	html: `
+		<button type="button">Set a-c</button>
+		<button type="button">Set b-c</button>
+		<button type="button">Set a-d</button>
+		<button type="button">Set b-d</button>
+	`,
+	async test({ assert, target, window, component }) {
+		const [btn1, btn2, btn3, btn4] = target.querySelectorAll('button');
+		const click = new window.MouseEvent('click');
+
+		await btn1.dispatchEvent(click);
+		assert.deepEqual(component.log, ['setKey(a, value-a-c, c)']);
+
+		await btn2.dispatchEvent(click);
+		assert.deepEqual(component.log, [
+			'setKey(a, value-a-c, c)',
+			'setKey(b, value-b-c, c)'
+		]);
+
+		await btn3.dispatchEvent(click);
+		assert.deepEqual(component.log, [
+			'setKey(a, value-a-c, c)',
+			'setKey(b, value-b-c, c)',
+			'setKey(a, value-a-d, d)'
+		]);
+
+		await btn4.dispatchEvent(click);
+		assert.deepEqual(component.log, [
+			'setKey(a, value-a-c, c)',
+			'setKey(b, value-b-c, c)',
+			'setKey(a, value-a-d, d)',
+			'setKey(b, value-b-d, d)'
+		]);
+	}
+};
diff --git a/test/runtime/samples/component-slot-context-props-each-nested/main.svelte b/test/runtime/samples/component-slot-context-props-each-nested/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-context-props-each-nested/main.svelte
@@ -0,0 +1,8 @@
+<script>
+	import Nested from './Nested.svelte';
+	export let log = [];
+</script>
+
+<Nested {log} let:set let:key let:item>
+	<button type="button" on:click={() => set(`value-${key}-${item}`)}>Set {key}-{item}</button>
+</Nested>
\ No newline at end of file
diff --git a/test/runtime/samples/component-slot-context-props-each/Nested.svelte b/test/runtime/samples/component-slot-context-props-each/Nested.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-context-props-each/Nested.svelte
@@ -0,0 +1,11 @@
+<script>
+	let keys = ['a', 'b'];
+  export let log;
+	function setKey(key, value) {
+		log.push(`setKey(${key}, ${value})`);
+  }
+</script>
+
+{#each keys as key (key)}
+	<slot {key} set={(value) => setKey(key, value)} />
+{/each}
\ No newline at end of file
diff --git a/test/runtime/samples/component-slot-context-props-each/_config.js b/test/runtime/samples/component-slot-context-props-each/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-context-props-each/_config.js
@@ -0,0 +1,19 @@
+export default {
+	html: `
+		<button type="button">Set a</button>
+		<button type="button">Set b</button>
+	`,
+	async test({ assert, target, window, component }) {
+		const [btn1, btn2] = target.querySelectorAll('button');
+		const click = new window.MouseEvent('click');
+
+		await btn1.dispatchEvent(click);
+		assert.deepEqual(component.log, ['setKey(a, value-a)']);
+
+		await btn2.dispatchEvent(click);
+		assert.deepEqual(component.log, [
+			'setKey(a, value-a)',
+			'setKey(b, value-b)'
+		]);
+	}
+};
diff --git a/test/runtime/samples/component-slot-context-props-each/main.svelte b/test/runtime/samples/component-slot-context-props-each/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-context-props-each/main.svelte
@@ -0,0 +1,8 @@
+<script>
+	import Nested from './Nested.svelte';
+	export let log = [];
+</script>
+
+<Nested {log} let:set let:key>
+	<button type="button" on:click={() => set(`value-${key}`)}>Set {key}</button>
+</Nested>
\ No newline at end of file
diff --git a/test/runtime/samples/component-slot-context-props-let/Inner.svelte b/test/runtime/samples/component-slot-context-props-let/Inner.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-context-props-let/Inner.svelte
@@ -0,0 +1,9 @@
+<script>
+  export let log;
+	function setKey(key, value) {
+		log.push(`setKey(${key}, ${value})`);
+  }
+</script>
+
+<slot key="a" set={setKey} />
+<slot key="b" set={setKey} />
\ No newline at end of file
diff --git a/test/runtime/samples/component-slot-context-props-let/Nested.svelte b/test/runtime/samples/component-slot-context-props-let/Nested.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-context-props-let/Nested.svelte
@@ -0,0 +1,8 @@
+<script>
+	import Inner from './Inner.svelte';
+  export let log;
+</script>
+
+<Inner {log} let:key let:set>
+	<slot {key} set={(value) => set(key, value)} />
+</Inner>
\ No newline at end of file
diff --git a/test/runtime/samples/component-slot-context-props-let/_config.js b/test/runtime/samples/component-slot-context-props-let/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-context-props-let/_config.js
@@ -0,0 +1,19 @@
+export default {
+	html: `
+		<button type="button">Set a</button>
+		<button type="button">Set b</button>
+	`,
+	async test({ assert, target, window, component }) {
+		const [btn1, btn2] = target.querySelectorAll('button');
+		const click = new window.MouseEvent('click');
+
+		await btn1.dispatchEvent(click);
+		assert.deepEqual(component.log, ['setKey(a, value-a)']);
+
+		await btn2.dispatchEvent(click);
+		assert.deepEqual(component.log, [
+			'setKey(a, value-a)',
+			'setKey(b, value-b)'
+		]);
+	}
+};
diff --git a/test/runtime/samples/component-slot-context-props-let/main.svelte b/test/runtime/samples/component-slot-context-props-let/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-slot-context-props-let/main.svelte
@@ -0,0 +1,8 @@
+<script>
+	import Nested from './Nested.svelte';
+	export let log = [];
+</script>
+
+<Nested {log} let:set let:key>
+	<button type="button" on:click={() => set(`value-${key}`)}>Set {key}</button>
+</Nested>