Repository: sveltejs/svelte (JavaScript)
Date: 2021-06-20 16:59:43+00:00
Task type: Bug Fix

Issue:
event handlers are unbound when called with forwarded DOM events from child components
**Describe the bug**
`this` is undefined in event handlers, when handling a DOM event which has been forwarded from a child component

**Logs**
See repro

**To Reproduce**
The bug can be easily reproduced in the [DOM Event Forwarding](https://svelte.dev/tutorial/dom-event-forwarding) section of  the tutorial. Click `Show me`, then replace the `handleClick` definition with:

```
	function handleClick(event) {
		alert(this);
	}
```

This caused confusion later in the tutorial, in the [Lifecycle/tick](https://svelte.dev/tutorial/tick) section, where `this` in a handler refers to `event.target`.

**Expected behaviour**
Unless this is intentional, I would expect consistency in how event handlers can be used regarding `this`. On the other hand, if using `this` from event handlers is discouraged, the above-mentioned `tick` section in the tutorial could use `event.target` instead

**Additional Context**
A discord user pointed me to https://github.com/sveltejs/svelte/blob/dafbdc286eef3de2243088a9a826e6899e20465c/src/runtime/internal/lifecycle.ts#L64-L70 where it appears the callbacks for the component events are invoked. If `event` here is the original DOM event, it may be as simple as calling the callback in the context of `event.target`:

```
fn.call(event.target, event)
```

In the event that `event.target` is undefined, we're back to where we started (`this` is undefined)


Issue discussion:
I'm not an expert here, but here's what I'm seeing:

In the `tick` example you're binding the event directly on `<textarea>` so you know what's happening and `this` behaves as expected. But I agree that it shouldn't use `this` there since it's also common to use arrow functions as event handlers and I personally always use `event.target`.

When you are using the `<CustomButton>` component, you have _no idea_ what `this` even means. It _could_ be a forwarded event, but you don't know from what type of element. But it could very well be a `dispatch('click')` without any DOM involved. What should `this` be then? It might make sense to have `this` point to the `CustomButton` instance?

So I'm with you that `this` should be discouraged I guess.
@Prinzhorn This issue is about preserving the `this` value for DOM forwarded events, so it matches the behavior of directly binding the event in the same component. Custom dispatched events would be expected to have `this` be `undefined`. 

But yeah, `this` is just confusing in general.
@bluwy what I meant is that a consumer of `CustomButton` does not know that it is a forwarded event. That's internal information. A realistic example would be a `Slider` component where the first iteration simply forwards the `click` event. But then in a later version custom custom `click` is triggered only when the cursor hasn't moved a certain threshold, so that sliding a slide doesn't also trigger the click. A consumer of `Slider` shouldn't care about this internal change. Hence you cannot rely on the internal information that this was originally a forwarded event. Does that make sense?
@Prinzhorn I'm not sure if that's true. One way or another, the consumer of the component would need to know if it's a forwarded DOM event or a dispatched event. This can't be hidden internally since the `Event` object is being interacted by the consumer. 

In case of a forwarded DOM event, the event type would be e.g. `MouseEvent`, while a dispatched event, it would be a `CustomEvent`. Both are of completely different shape and it would be a breaking change if you decide to swap between the two. 

Solution patch:
diff --git a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts
--- a/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts
+++ b/src/compiler/compile/render_dom/wrappers/Element/EventHandler.ts
@@ -20,7 +20,7 @@ export default class EventHandlerWrapper {
 
 			this.parent.renderer.component.partly_hoisted.push(b`
 				function ${node.handler_name.name}(event) {
-					@bubble($$self, event);
+					@bubble.call(this, $$self, event);
 				}
 			`);
 		}
diff --git a/src/runtime/internal/lifecycle.ts b/src/runtime/internal/lifecycle.ts
--- a/src/runtime/internal/lifecycle.ts
+++ b/src/runtime/internal/lifecycle.ts
@@ -65,6 +65,7 @@ export function bubble(component, event) {
 	const callbacks = component.$$.callbacks[event.type];
 
 	if (callbacks) {
-		callbacks.slice().forEach(fn => fn(event));
+		// @ts-ignore
+		callbacks.slice().forEach(fn => fn.call(this, event));
 	}
 }


Test patch:
diff --git a/test/runtime/samples/component-events-this/Inner.svelte b/test/runtime/samples/component-events-this/Inner.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-events-this/Inner.svelte
@@ -0,0 +1,7 @@
+<script>
+  import { createEventDispatcher } from 'svelte';
+  const dispatch = createEventDispatcher();
+
+</script>
+
+<button on:click={() => dispatch('bar')} />
\ No newline at end of file
diff --git a/test/runtime/samples/component-events-this/Widget.svelte b/test/runtime/samples/component-events-this/Widget.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-events-this/Widget.svelte
@@ -0,0 +1,6 @@
+<script>
+  import Inner from './Inner.svelte';
+</script>
+
+<button on:click>click me</button>
+<Inner on:bar />
\ No newline at end of file
diff --git a/test/runtime/samples/component-events-this/_config.js b/test/runtime/samples/component-events-this/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-events-this/_config.js
@@ -0,0 +1,11 @@
+export default {
+	test({ assert, component, target, window }) {
+		const [button1, button2] = target.querySelectorAll('button');
+
+		button1.dispatchEvent(new window.MouseEvent('click'));
+		button2.dispatchEvent(new window.MouseEvent('click'));
+
+		assert.strictEqual(component.logs[0], button1);
+		assert.ok(component.logs[1] instanceof component.Inner);
+	}
+};
diff --git a/test/runtime/samples/component-events-this/main.svelte b/test/runtime/samples/component-events-this/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-events-this/main.svelte
@@ -0,0 +1,16 @@
+<script>
+	import Widget from './Widget.svelte';
+	import Inner from './Inner.svelte';
+
+	export let logs = [];
+	export { Inner };
+
+	function foo() {
+		logs.push(this);
+	}
+	function bar() {
+		logs.push(this);
+	}
+</script>
+
+<Widget on:click={foo} on:bar={bar} />
\ No newline at end of file