Repository: sveltejs/svelte (JavaScript)
Date: 2022-09-13 05:40:44+00:00
Task type: Feature

Issue:
Feature Request: Allow nullish values to omit event handlers
### Describe the problem

Most props can be excluded in this sort of fashion:
```svelte
<MyComponent
  prop={condition ? someValue : undefined}
/>
```

However, this does not work for event handlers (at least in TS setups) due to the expected TypeScript type `(e: MouseEvent) => any`.

### Describe the proposed solution

I propose setting up event handlers in such a way that they support the types `null | undefined` as well as `(e: MouseEvent) => any` and that in the case of `null | undefined`, the handler be excluded from the element/component instance.

### Alternatives considered

Currently, to work around this, I am using an "empty" function with nullish coalescing. This won't kill site performance by any means, but firing a pointless function like this is not the ideal workaround.

Here is an example of my current setup:
```svelte
<MyComponent
  prop={condition ? someValue : undefined}
  on:click={clickFunction ?? (() => {})}
/>
```

### Importance

would make my life easier


Issue discussion:
@dummdidumm I think this would be a `language-tools` thing? Is that correct? The compiler itself already allows nullish values for event handlers, and treats them as not attaching a handler.
Yes this needs a adjustment in language tools. This is already allowed for Dom events but not component events 
Thanks, @Conduitry and @dummdidumm!
@Conduitry was about to implement this and tested it out in the REPL one more time, and noticed that this isn't implemented for component events. If I do `<Component on:click={null} />`, an error is thrown every time I fire the click event. The "handles null/undefined" behavior seems to be implements for DOM events only.

Solution patch:
diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts
--- a/src/runtime/internal/Component.ts
+++ b/src/runtime/internal/Component.ts
@@ -212,6 +212,9 @@ if (typeof HTMLElement === 'function') {
 
 		$on(type, callback) {
 			// TODO should this delegate to addEventListener?
+			if (!is_function(callback)) {
+				return noop;
+			}
 			const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
 			callbacks.push(callback);
 
@@ -244,6 +247,9 @@ export class SvelteComponent {
 	}
 
 	$on(type, callback) {
+		if (!is_function(callback)) {
+			return noop;
+		}
 		const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
 		callbacks.push(callback);
 


Test patch:
diff --git a/test/runtime/samples/component-events-nullish/Widget.svelte b/test/runtime/samples/component-events-nullish/Widget.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-events-nullish/Widget.svelte
@@ -0,0 +1,14 @@
+<script>
+	import { createEventDispatcher } from 'svelte';
+	const dispatch = createEventDispatcher();
+	export let logs;
+
+	function click() {
+		try {
+			dispatch('click');
+		} catch (error) {
+			logs.push(error);
+		}
+	}
+</script>
+<button on:click={click} />
\ No newline at end of file
diff --git a/test/runtime/samples/component-events-nullish/_config.js b/test/runtime/samples/component-events-nullish/_config.js
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-events-nullish/_config.js
@@ -0,0 +1,9 @@
+export default {
+	async test({ assert, component, window, target }) {
+		const event = new window.MouseEvent('click');
+		const button = target.querySelector('button');
+		
+		await button.dispatchEvent(event);
+		assert.equal(component.logs.length, 0);
+	}
+};
diff --git a/test/runtime/samples/component-events-nullish/main.svelte b/test/runtime/samples/component-events-nullish/main.svelte
new file mode 100644
--- /dev/null
+++ b/test/runtime/samples/component-events-nullish/main.svelte
@@ -0,0 +1,6 @@
+<script>
+	import Widget from './Widget.svelte';
+	export let logs = [];
+</script>
+
+<Widget on:click={null} {logs} />