Svelte 5: Include event modifiers previously found pre svelte 5 as functions available from the svelte library
Describe the problem
I would like for the functionality of event modifiers previously available before svelte 5 for example 'click', 'once' and 'preventDefault' (shown below) to be readily available in svelte 5.
<button on:click|once|preventDefault={handler}>...</button>
Describe the proposed solution
In the preview documentation it suggests creating functions to replace event modifiers.
So the following:
<button on:once|preventDefault={handler}>...</button>
would be rewritten as:
<button onclick={once(preventDefault(handler))}>...</button>
In my opinion, it would be nice if these functions were available from the svelte library itself rather than having to write them independently. This would bring parity between svelte 4 in and svelte 5 for this feature and help minimise work needed for migration.
I understand the addition of these functions needs to be considered against the trade-off of additional bloat to the library, especially as these are relatively quick and simple functions to write (though ensuring the typing is correct adds some difficulty in my experience).
I would be happy to put in a PR for the change if it is deemed a worthwhile addition.
Importance
nice to have
while agree to this, I'll share some ideas.
It don't have to be nested(wrapping(a(handler))), we can use chaining as previously instead
<button onclick={ callable(handler).once.preventDefault }>click me!</button>
It utilizes Proxy internally. here the preview
I agree; it should be done somehow. Keeping it in legacy with "deprecated" is also not really good. Here https://svelte.dev/docs/svelte/v5-migration-guide#Event-changes-Event-modifiers, I disagree with the argument:
Adding things like event.preventDefault() inside the handler itself is preferable, since all the logic lives in one place rather than being split between handler and modifiers.
I think "business logic" and event process is a completely different case, instead:
<a href="" on:click|preventDefault={() => setPage(currentPage + 1)}>Next</a>
I need to do:
<a href="" onclick={(event) => { event.preventDefault(); setPage(currentPage + 1); }}>Next</a>
Or, in that case, I should create a new function on top of what I already have, like setPagePreventDefault.
@yus-ham I suppose it's a good proposal, but not so cool if you put an arrow function instead of just the handler name.
I just tried to put the modifiers at the beginning and came up with this:
<a href="" onclick={event.preventDefault.once(() => {console.log('Hello world')})}>foobar</a>
This works in most cases, but not all. You cannot create passive/nonpassive modifiers this way.
You can find the code here.
Why is the once considered deprecated?
<script>
// 'once' is deprecated.js(6385)
import { once } from 'svelte/legacy';
</script>
<button
onclick={once((e) => {
e.preventDefault();
handler(e);
})}>...</button
>
Event the docs suggests making the once wrapper.
Since event handlers are just functions, you can create your own wrappers as necessary:
function once(fn) {
return function (event) {
if (fn) fn.call(this, event);
fn = null;
};
}
Note that this is different from the svelte/legacy implementation:
/**
* Substitute for the `once` event modifier
* @deprecated
* @param {(event: Event, ...args: Array<unknown>) => void} fn
* @returns {(event: Event, ...args: unknown[]) => void}
*/
export function once(fn) {
var ran = false;
return function (...args) {
if (ran) return;
ran = true;
// @ts-ignore
return fn?.apply(this, args);
};
}
I would love to hear why this is omitted in Svelte 5. They are among most useful functionalities in Svelte all along.
So events can just be plain properties that can be passed around and spread on elements/components.
Given that the svelte/legacy module includes the relevant wrapper functions, this issue can probably be closed.
Everything in svelte/legacy is deprecated. So those work for now, but we shouldn't be encouraging people to use them longer term. What this issue is asking for is non-deprecated helper functions, and we need to decide whether that's a pattern we want to encourage. We'd previously decided 'no', and if we still think 'no', then that's the reason we should close this issue - not because we already have deprecated functions people can use.
If the best example available of their utility is this...
<a href="" on:click|preventDefault={() => setPage(currentPage + 1)}>Next</a>
...then that to me is a pretty good reason not to include them. That is a bad pattern and you shouldn't be doing that! Links are supposed to go somewhere, not do something. What you have there is a button, and if it had been implemented with a <button> you wouldn't have needed to prevent the default behaviour. (Of course, better still would be to keep it as a link, but get rid of the onclick altogether and use a router for pagination, but that's likely a much bigger change.)
In my experience this pattern generalises: if you need these sorts of modifiers frequently, it's probably a sign that there's a better way to solve the problem at hand. The job of a framework isn't to make everything easier, it's to make the right thing easier. And there's a cost to adding new APIs, and the benefit has to outweigh that cost.
For those reasons we initially chose not to include these helpers out of the box, and I haven't yet seen any compelling evidence that it was the wrong choice.
I'm going to take Rich's comment as my cue to close this issue. I'm happy with the decision and thank you to the svelte team for taking the time to consider the proposal
I find myself using on:click|self often, pre Svelte 5, to ensure the correctness of the component doesn't rely on other components calling e.stopPropagation(). Is this also an anti-pattern? What'd be the Svelte 5-esque way for this?
I currently do if (e.target === e.currentTarget)