rfcs
rfcs copied to clipboard
Implement forward directive
Apology for the rather brief document; i'm not especially familliar with svelte's internals.
@benmccann also proposed in https://github.com/sveltejs/svelte/pull/4599 an alternative bubble directive, which serves as a syntax replacement for traditional event forwarding in Svelte 4. This RFC could also encapsulate that using a syntax such as forward:on:foo, or even forward:on:foo={sideEffectFunction} to avoid dispatching when working with event side-effects.
We could even extend this concept of taking side-effect arguments in forward to actions and transitions, so forward:use:internalAction would foward the use directive as well as apply an internal action, although this somewhat complicates matters of whether devs should be allowed just use the native use syntax along with forward:use.
Have updated with thoughts from my comment above. Let me know if this should be split into a separate RFC at some point.
I like this a lot!
I'd definitely opt for a more separated thing, not allowing forward:use:internalAction. Better to have it separate to make it clear what is going on IMO.
Just re-read the part on Additional Proposal - foward effects and singular event forwarding. This is trivial to get around at the moment for actions: <button forward:use use:action />. The same can be done with events: <button forward:on on:click={internalFunction} />.
Where it might make sense is in the following situation where you want to run a function on all events: <button forward:on={internalFunction} /> vs <button forward forward:on on:*={internalFunction}.
I like that it also solves forwarding use and even multiple of them.
I really like forwarding specific events and actions (maybe transitions and animations too?) with forward:on:event & forward:use:action. Should definitely make it in.
I really like forwarding specific events and actions (maybe transitions and animations too?) with
forward:on:event&forward:use:action. Should definitely make it in.
forward:on:event is not needed, you can already do that today by just writing on:event :)
Using forward is a little more explicit though. When I first saw on:event being used, I had no idea what it means. With forward:, it's clear that you're forwarding the event.
I really like [...] &
forward:use:action. Should definitely make it in.
What should forward:use:action do? action is a function that the component author specifies. It gets executed, and then?
I guess the way to go would be forward:use use:action.
As for the forward:on:click={handler}, the only thing that could be achived with it instead of on:click on:click={handler} would be, if it does not get forwarded if the handler would call event.stopImmediatePropagation() (I hope this sentence makes sense).
I don't think we need any extra syntax for exposing transitions/actions. You can expose that through props alreay, which is top-down, and introducing a forward-syntax for that, which would be bottom-up, is confusing to me. So the RFC would be reduced to forward of events, at which point the question is if bubble is the better wording.
The whole last part involving Svelte 4 was based on some discussion I saw in the on:* PR in which @benmccann suggested a more explicit syntax for forwarding events rather than not passing a handler. I agree that we should avoid using double directives as much as possible, and if there's a way to do that that's also easy to understand, i'm all in.
As @kaisermann said, we can also alter the side-effect syntax to use both the on/transition/use and forward directives:
<button forward:on:click on:click={internalFunction}></button>
<button foward:in in:internalTransition={}></button>
We could also just ignore the additional proposal and only use forward as a method of routing the whole directive rather than a specific event type, which would avoid "double directives" completely, which is what i'm for initially.
I don't think we need any extra syntax for exposing transitions/actions. You can expose that through props alreay, which is top-down, and introducing a
forward-syntax for that, which would be bottom-up, is confusing to me. So the RFC would be reduced toforwardof events, at which point the question is ifbubbleis the better wording.
I don't think this is as clear cut as you make it out to be. For transitions I see your point and I think I agree but for actions it's a bit trickier. This makes it a lot easier to apply multiple actions, rather than having to pass in an array of them into some prop. Applying multiple actions dynamically is not very ergonomic at the moment - you have to write a utility action that takes an array of actions. Compare these two as a consumer of the component:
<MyInput use:autoResize={400} use:copyOnClick={{ once: true }} />
vs
<MyInput actionsToApply=[{action: autoResize, props: 500}, {action: copyOnClick, props: { once: true }}]
Another thing it has going for it is that immediately obvious that there is an action or transition applied to the component, I think that's worth something.
I don't think we need any extra syntax for exposing transitions/actions. You can expose that through props alreay, which is top-down, and introducing a
forward-syntax for that, which would be bottom-up, is confusing to me. So the RFC would be reduced toforwardof events, at which point the question is ifbubbleis the better wording.
I did discuss this slightly in the RFC itself, but i'm fairly against the idea of leaving it to the developers of component libraries to abstract away the svelte language syntax. It's painful for the library's users since there can and will be inconisistencies, and it's painful for the developers, since the method isn't especially obvious and is a whole lot of boilerplate to do something extremely simple. People know how to use the native syntax, so why not use it?
Regarding actions/passing in stuff: This is true for the case where you want to expose one specific element, but fails when you want to be able to apply actions/transitions/whatever to multiple DOM elements, at which point it could get more confusing. Conceptionally, an DOM element consists of - well - one element, whereas for components it could be 0-x.
Regarding "leave it to authors to abstract away svelte language syntax": I don't think this is particulary painful. Writing export let transition; ... transition:transition is only a couple more keystrokes than forward:transition.
I feel your desire to have a uniform API (this actions=[{action: .., x: ..,y: ..} doesn't feel very idiomatic), but I don't think it's that tradeoff-free as it is presented here.
Regarding actions/passing in stuff: This is true for the case where you want to expose one specific element, but fails when you want to be able to apply actions/transitions/whatever to multiple DOM elements, at which point it could get more confusing. Conceptionally, an DOM element consists of - well - one element, whereas for components it could be 0-x. Regarding "leave it to authors to abstract away svelte language syntax": I don't think this is particulary painful. Writing
export let transition; ... transition:transitionis only a couple more keystrokes thanforward:transition. I feel your desire to have a uniform API (thisactions=[{action: .., x: ..,y: ..}doesn't feel very idiomatic), but I don't think it's that tradeoff-free as it is presented here.
This doesn't necessarily invalidate the use of use props, it just provides an easier pathway for the most common scenario. $$restProps is a similar example in my eyes. You could export a prop for every possible attribute someone might need or you could use the convenient solution and route all of the attributes to a single DOM element in your template.
Although custom action syntax might not seem like a lot, it ends up stacking up when you have to deal with every edge case over a large amount of components.
What should
forward:use:actiondo?actionis a function that the component author specifies.
that's not necessarily true, as sometimes you would want to pass an/multiple actions into the component
but fails when you want to be able to apply actions/transitions/whatever to multiple DOM elements
is that any different from forwarding multiple on:click events in one component?
edit: yes, but basically it would run for every element
and I think people will always build code which doesn't do what I would expect, but they can be teached
I really don't see the problem with having a uniform, really dynamic way of passing things down. It is obvious that if you want to modularize things furthest, you NEED all actions, all events, all transitions, all props, all bindings (and so on).
The question is more how to do that without too much development/maintenance overhead and a simple, idiomatic API.
So, i've updated the RFC:
At the time of opening this PR, I was actually unaware that an element could take in more than one on directive of the same type, meaning you can both forward something like a click event and use an internal function without dispatching. This drastically simplifies the implementation and removes the need for some of the syntax people are opposed to.
So i've removed the proposal for this sort of syntax:
<button forward:use:internalAction={}></button>
and replaced it with the equivalent
<button foward:use use:internalFunction={}></button>
This could be expanded to cover the use case for forwarding CSS variables,
<!--Button.svelte-->
<button forward:--btn-color></button>
<!--Consumer.svelte-->
<Button --btn-color="#007FFF" />
Unless I'm missing something, this works around the issue of css vars on components potentially messing up the css structure with the extra div, since we now know exactly which element to put style="--btn-color: #007FFF;" on. Makes themable components a lot more easier.
@698969 I think that's also interesting, but might be better to think about later?
What I found really confusing was that you can do css-vars on a component but not on an element like
<button --btn-color="#007FFF"> but that's off-topic
This could be expanded to cover the use case for forwarding CSS variables,
<!--Button.svelte--> <button forward:--btn-color></button><!--Consumer.svelte--> <Button --btn-color="#007FFF" />Unless I'm missing something, this works around the issue of css vars on components potentially messing up the css structure with the extra div, since we now know exactly which element to put
style="--btn-color: #007FFF;"on. Makes themable components a lot more easier.
I've discussed this in the discord a bit, but I think handling style props is out of the scope of this RFC. Recently, the PR for the style: directive was approved and it'll likely be merged soon, so this proposal can handle forwarding style directives, however I believe the forward: syntax should keep the single consistent purpose of forwarding directives in components. Introducing a one-size-fits-all syntax for handling directives and style props (classes were proposed too at some point) would just make things more difficult to understand.
Personally I really like this direction and think that something like this is sorely needed in Svelte.
What are the semantics around event dispatching? eg if I have something like
// MyComponent.svelte
...
dispatch("update", value);
...
<ChildComponent forward:on />
If I do <MyComponent on:update={...} />, are we listening to an update dispatched from MyComponent? From ChildComponent? Both?
@rgossiaux what happens if you currently do this, but forward only update? edit: https://svelte.dev/repl/50b98a9d3d2a46e7b2ba1c1eff063130?version=3.44.3 so it sends both (and keeps the order, lowest first)
Personally I really like this direction and think that something like this is sorely needed in Svelte.
What are the semantics around event dispatching? eg if I have something like
// MyComponent.svelte ... dispatch("update", value); ... <ChildComponent forward:on />If I do
<MyComponent on:update={...} />, are we listening to anupdatedispatched fromMyComponent? FromChildComponent? Both?
Did a little testing over how Svelte handles this currently with conventional forwarding, and it's both: https://svelte.dev/repl/6984269f1c9d428b91412ef93d063195
Worth mentioning it in the RFC if that's how forward: should behave too. I called this out because I think that won't necessarily be desired all the time with custom events. In my example, the author of this component certainly intended to expose the update event from the outermost component, and they may wish to use forward: to forward all DOM event handlers to a lower component, but they might not have intended to expose any internal update events further down the tree. Any component that both dispatches events and uses svelte:self or otherwise includes itself may run into this, for example, in a way that might be annoying to resolve.
Some possibilities for dealing with this case:
- Ignore it for now. It won't be a problem most of the time, especially if your component doesn't make much use of custom events.
- Provide some way to only forward DOM event listeners. Then custom events could be forwarded individually with
on:. The set of custom events for a component will be known and finite (and presumably small). But this is also a bit gnarly since now the framework is making some decisions about if your event is a DOM one or not. - Provide some way to exclude individual event names from
forward:on. I kind of like this option the most as long as it's not too unwieldy. - Do forwarding in a different way from
forward:onentirely. For example, an alternate way would be for the event handlers to be available to the component in some object which you could spread onto a child component, which would give you full control over which handlers to forward or not forward.
to forward all DOM event handlers to a lower component
isn't forward meant the other way around? (forward the event up the tree)
anyways, excluding seems like it would be useful, say you want a component to handle click in some way and dispatch it explicitly afterwards (or with changed event details).
I thought about syntax, which is a little unwieldy, though that might be okay as you would mostly only exclude some events:
<button forward:on exclude:on:click/>
<!-- or, to serve my example above: -->
<button forward:on exclude:on:click={(e) => dispatch("click", doSomething(e))}/>
to forward all DOM event handlers to a lower component
isn't forward meant the other way around? (forward the event up the tree)
anyways, excluding seems like it would be useful, say you want a component to handle click in some way and dispatch it explicitly afterwards (or with changed event details).
I thought about syntax, which is a little unwieldy, though that might be okay as you would mostly only exclude some events:
<button forward:on exclude:on:click/> <!-- or, to serve my example above: --> <button forward:on exclude:on:click={(e) => dispatch("click", doSomething(e))}/>
There's a slight issue with this in particular, notably the fact that you are using this exclude directive on a specific event instance, not an event name. I think that forward:on={{ exclude: ["click"] }} might make more sense in the context of an API. If we do decide to exclude event instances, on:click|exclude might work too and not require the addition of a new directive. Would be interested to hear other people's thought's on this.
just realized exclude:click would be shorter.
I don't really like strings there (intellisense), second example is good.
This feature implies that the Svelte compiler should know all the event types of any element, and the namespace of element used. Is button in HTML deserve this feature more than circle in SVG?
Not sure if this needs an explicit update to the RFC, but this could also fit in with the style: directive now that it's merged.