Skip to content

Svelte 5: event handling makes it harder to understand #10645

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
hgiesel opened this issue Feb 26, 2024 · 4 comments
Closed

Svelte 5: event handling makes it harder to understand #10645

hgiesel opened this issue Feb 26, 2024 · 4 comments

Comments

@hgiesel
Copy link
Contributor

hgiesel commented Feb 26, 2024

Describe the problem

I'd argue, that getting rid of the concept of events in Svelte 5 increases the complexity, rather than decreasing it. It's true, that it's one less thing to learn, but events also enforce a specific component design pattern:

<My.Component on:action={doHandle} />

Just by looking at this piece of code, we now that doHandle returns void, or at least, the components discards the return value of doHandle. Also we know, that doHandle will not be waited for in the case it is async.

If we do it the Svelte 5 way:

<My.Component onAction={doHandle} />

All of this implicit knowledge on how onAction operates is lost. We need to look at the code of My.Component. All of this can be remedied by documentation, but it's to have this knowledge without having to look at docs.

Additionally, when creating the props for event handlers, more typing boilerplate is required.

Describe the proposed solution

Reintroduce explicit events with the $events rune. I think it could have a similiar api as Vue's defineEmits.

<script lang="ts">
let favoriteNumber = $state(42)

const emit = $events<{
  success: [favoriteNumber: number]
  cancel: []
}>()
</script>

Enter your favorite number:
<input type="number" bind:value={favoriteNumber} />
<button on:click={() => emit("success", favoriteNumber)}>Commit</button>
<button on:click={() => emit("cancel")}>Cancel</button>

This would behave the same as:

<script lang="ts">
let favoriteNumber = $state(42)

let { onSuccess, onCancel } = $props<{
  onSuccess: (favoriteNumber: number) => void,
  onCancel: () => void, 
}>()
</script>

Enter your favorite number:
<input type="number" bind:value={favoriteNumber} />
<button onclick={() => onSuccess(favoriteNumber)}>Commit</button>
<button onclick={onCancel}>Cancel</button>

Importance

would make my life easier

@jonshipman
Copy link

jonshipman commented Feb 26, 2024

This is what I've been doing.

import type { Action } from 'svelte/action';

function isListenerEvent<T = never>(event: Event): event is CustomEvent<T> {
  return 'detail' in event && typeof event.detail === 'object' && !!event.detail;
}

type Options = {
  event: string;
  callback: (detail: never) => void;
};

export const listener: Action<HTMLElement, Options> = function (node: HTMLElement, options) {
  function listen(event: Event) {
    if (isListenerEvent(event)) {
      event.stopImmediatePropagation();
      options.callback(event.detail);
    }
  }

  node.addEventListener(options.event, listen);

  return {
    destroy() {
      node.removeEventListener(options.event, listen);
    }
  };
};

export function dispatch(node: HTMLElement, event: string, payload: unknown) {
  node.dispatchEvent(
    new CustomEvent(event, {
      bubbles: true,
      composed: true,
      cancelable: true,
      detail: payload
    })
  );
}

Have that in a listener.ts file. Then in my components I can just call dispatch(node, 'someeventname', {...data}) and somewhere up top, I can call <div use:listener={{event: 'someeventname', callback(detail) { console.log(detail); }}} />

A) uses native JavaScript events so you can pass details as high up as you want. B) will be compatible with svelte unless they remove the use:action directives.

Edit: Svelte5 Dev link in action: https://svelte-5-preview.vercel.app/#H4sIAAAAAAAACo1TwY6bMBD9lVkOC5EQuZMQqVr1UKmVeuit9GDwkDprbGQPaSPEv1e2MWSze-jJ8sz4vTdvxlPSCYk2KX9OiWI9JmXyaRiSPKHb4C72ipIwyROrR9O6yNG2Rgx0qlVNoh-0IZhACkuo0OTAhR0Ytb9hhs7oHtJiH5PFxaaHWh33G4I6cnF1SKPFMtZV0-RCNeEVFZWQElpK8xBrmZQNa18zjsSE3MFSW1OrldUSC6nPMXkIudkd81wrL_rYjERahZRWrRTtazVlnmwH1WlDjL2EXNGOxqCiH8yckfIoCybotC4hbZhJYV45PekpXF4cB3xDT78P_CfnBBfXU62SPOk1F51AnpRkRpzzdRrRlG0kF3s_jm5ULQmtQNivS-lnpzb245sxSKNRkAZbUhAKfBqen8Gh6i7ci1AAVVVBqpsLtpS6mqen-_ShVq45hX_99FcFUWqmNMcc9OCidpHwUPVGXk2ig-zjBu4GHDRY0sOXvkcuGOF3owd2Zg44i87XtDAX667cq3-7FLXfAye4YJx74qgiizD-db4I9883SxdxHC0Zfcvu1HpMg72-4v_Crqqcw4ePLF43Mli8QAzsJjXjC7knjoXByoCt8A-8jJZ0f2dwviluxqaRaEtwK5hv36oftEX-GGaqRckaiQ-J4HIZRS1t7dy5C5vzft1_zf8AY5F1BYgEAAA=

@hgiesel hgiesel changed the title Svelte 5 event handling makes it harder to understand Svelte 5: event handling makes it harder to understand Feb 26, 2024
@hgiesel
Copy link
Contributor Author

hgiesel commented Feb 26, 2024

This is what I've been doing. [...]

This only works for events on HTML elements. Even then, it looks terribly awkward. I think it shows even more, why we need on: back.

@dummdidumm
Copy link
Member

The advantages (spreading events, more flexibility and power how to compose/use them, no .detail boilerplate) outweigh the personal preferences here. Conventions like onX will surely make this a non-issue in practice. We're not going to change this, therefore closing.

@dummdidumm dummdidumm closed this as not planned Won't fix, can't repro, duplicate, stale Feb 26, 2024
@hgiesel
Copy link
Contributor Author

hgiesel commented Feb 26, 2024

Just for completion sake, I'd like to go over the supposed advantages:

reduce Svelte's learning curve

As I said above, it does decrease the learning curve, but at the cost of readability and easily understood conventions.

I'm pretty sure the new convention in most people's code will be: "If it's a function, call it onX", and whether it's actually an event handler, or a function that changes how the component operates (e.g. because it returns a value, or it is awaited), will be something you have to look up.

Examples of how users already don't follow this convention:

remove boilerplate, particularly around createEventDispatcher

The extra boilerplate is now in defining the types of the callbacks.

remove the overhead of creating CustomEvent objects for events that may not even have listeners

Fully agree. No need for CustomEvent objects, just pass the values directly to the callback. This also means no need for .detail.

add the ability to spread event handlers

This could be done with a variant of the $events rune:

<script lang="ts">
const myEvents = $events.pass<{
  a: [number]
}>()

// Without typescript:
// const myEvents = $events.pass("a")

const otherEvents = $events.pass<{
  b: [string]
}>()
// const otherEvents = $events.pass("b")
</script>

<My.Component on:myEvents />
<Other.Component on:otherEvents />

Honestly, this is even better than what is suggested by Svelte 5 currently, because this doesn't assume you want to pass all of the event handlers to a single other component.

EDIT:

Also, if $events and on: would be just syntactic sugar on top of event handlers as props, it would make it even easier.

on:x={f} used on an Svelte component expands to onX={f}, and on:x={f} on a HTML element expands to onx={f}. Now, you can still spread as you want to, but make it easier to keep conventions and make things explicit.

<script lang="ts">
import { HTMLInputProps } from "svelte/elements"

const { inputAttrs = {} } = $props<{
    inputAttrs: HTMLInputProps
}>

const emit = $events<{
    commit: [],
}>()
</script>

<input {...inputAttrs} />
<button on:click|preventDefault={() => emit("commit")}>Commit</button>

add the ability to know which event handlers were provided to a component

This could be easily implemented:

<script lang="ts">
const emit = $events<{
  handle: []
}>()
</script>

{#if emit.handle}
  <!-- Do your thing -->
{/if}

This would mean that emit.handle() and emit("handle") are equivalent in the case that the event handler for handle exists.

add the ability to express whether a given event handler is required or optional

Event handlers are by definition optional. That's also how HTML works. There are no required event handlers in HTML.

increase type safety (previously, it was effectively impossible for Svelte to guarantee that a component didn't emit a particular event)

I don't understand this one.

Examples of other users who don't like the removal of on:

The only issue I have is even modifiers, if anyone from the svelte team reads this please add it back, I am relying on event modifiers extensively and I don't want to call event.... every time, I get the reasoning behind removing it but it's a good QOL improvement

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants