rfcs
rfcs copied to clipboard
Declarative Actions
It seems like this is already possible by wrapping the element with a component and slot props -- maybe use:Component is just syntactical sugar for wrapping an element with a component?
It seems like this is already possible by wrapping the element with a component and slot props -- maybe use:Component is just syntactical sugar for wrapping an element with a component?
How would slot props solve the problem? And a component can't really assume its slot only has one child.
It seems like this is already possible by wrapping the element with a component and slot props -- maybe use:Component is just syntactical sugar for wrapping an element with a component?
How would slot props solve the problem? And a component can't really assume its slot only has one child.
How would they not? What would this make possible that would not otherwise be possible by wrapping an element with a component? I mention slot props because use:Component
might assist in facilitating the flow of data there.
To your point about a slot containing multiple child nodes, perhaps in the context of use:Component
we could expect one or more nodes, or throw an error for multiple child nodes. Taking an array might not be a bad thing... what if I wanted the action to apply to all child nodes?
It seems like this is already possible by wrapping the element with a component and slot props -- maybe use:Component is just syntactical sugar for wrapping an element with a component?
How would slot props solve the problem? And a component can't really assume its slot only has one child.
How would they not? What would this make possible that would not otherwise be possible by wrapping an element with a component? I mention slot props because
use:Component
might assist in facilitating the flow of data there.To your point about a slot containing multiple child nodes, perhaps in the context of
use:Component
we could expect one or more nodes, or throw an error for multiple child nodes. Taking an array might not be a bad thing... what if I wanted the action to apply to all child nodes?
Check out the "More on styling" section of the RFC. How would you do that with a wrapper component and slot props? I have an example with a wrapper Component. Applying styles to the parent is not the same as applying styles to the Element itself. How would you solve the problem I propose there? Could you spin up an example on the REPL?
Regarding this:
what if I wanted the action to apply to all child nodes?
Then it wouldn't be an action anymore. That is in no way possible with current actions either. If you truly wanted that you could apply the action individually to all the elements you wanted I guess.
Check out the "More on styling" section of the RFC. How would you do that with a wrapper component and slot props? I have an example with a wrapper Component. Applying styles to the parent is not the same as applying styles to the Element itself. How would you solve the problem I propose there? Could you spin up an example on the REPL?
The use of :global
isn't exactly awesome, but there are ways:
https://svelte.dev/repl/7b0c6c778aa04b2da0e862a71721c187?version=3.30.0
Then it wouldn't be an action anymore.
True.
The use of :global isn't exactly awesome, but there are ways: https://svelte.dev/repl/7b0c6c778aa04b2da0e862a71721c187?version=3.30.0
Ah, I see. One of my intentions with this RFC was to avoid :global
as with global css there are, indeed, ways.
That solution is in fact better looking than the ones I proposed as possible alternatives. I'll add the PassiveModifier
(even though it assumes a single child) approach to the the "More on Styling" section when I have some time, probably later today.
(there might be a way to style parent Elements in the future, :has)
But apart from styling I still do think Declarative Actions are a generally better way to write Actions.
I can certainly see the appeal, I was just thinking it might be possible to deliver the reactivity you seek without introducing the <target />
element and a special paradigm for reactive actions by leveraging components instead.
It's too bad you can't bind:this
to a component's let:
declaration, or this reactivity would be a lot easier with a component wrapper. But what if you could? What if a component could assert that it only takes a single child? What if onMount (or some other mechanism) provided the refs of child elements? Might there be a less complicated path to this reactivity within components?
There might be other ways. Introducing a new Element (or using <slot />
) will always be necessary for styling though, since Svelte will delete non used styles. Without relying on global styling that is.
EDIT:
As I said in the RFC, an alternative could be to introduce <target />
for use in Components. This could cannibalize actions though.
One thing that is not clear to me is how well the proposed type of actions will be composable. A big strength of the current actions (and a reason to keep the "old" way in, even if this RFC would be implemented) is that they are just functions and can easily be mixed and matched with other functions or even actions.
Hi, Sorry for being late to the party. I am the author of the most popular (afaik) drag and drop library, svelte-dnd-action, which uses an action (that is actually aggregating two other actions). The team that I am leading at work also uses svelte for some of our production apps and we write custom actions very frequently. imo, actions are one of the most powerful and unique features svelte offers (and in many ways svelte's answer to React hooks) and replacing it would be a huge step backwards. The define a very clean interface for how javascript code can interact and be integrated seamlessly with svelte applications. The author of the action gets (almost) unlimited control and the consumers of the action get a tiny, elegant API. Here is when we reach out for actions in their current form (outside of the obvious usecases):
- actions in their current form are not limited to reasoning about a single component. they can be used to tie components together (ex: holding a map with references to each component that uses the action and triggering events on any of them whenever it sees fit). It can address infinitely complex usecases yet, from the perspective of the consumer of the action, there is a nice, compact, declarative API that completely encapsulates all of the implementation details.
- When we want to implement functionality that can be easily ported to be used with any library/ framework/ vanilla js. Because actions don't need svelte as a dependency they can easily be wrapped with a React hook or be used directly (calling update when needed) in any codebase. I experimented with it here and we do it for one of the internal libraries we've created at work.
If you go with this please do it in addition and not instead the existing API.
@IsaacHagoel I would never have thought that this proposal is to replace the existing API. But I'm not a decision maker, just a fan and user of Svelte.
One thing that is not clear to me is how well the proposed type of actions will be composable. A big strength of the current actions (and a reason to keep the "old" way in, even if this RFC would be implemented) is that they are just functions and can easily be mixed and matched with other functions or even actions.
Composition should be fairly simple since since <target />
, or any other Element for that matter, can have multiple actions applied to it.
<!-- blueText.svelte -->
<style>
.blue {
color: blue;
}
</style>
<target class:blue />
<!-- redBg.svelte -->
<style>
.red {
background-color: red;
}
</style>
<target class:red />
<!-- composition.svelte -->
<script>
import { redBg } from "redBg.svelte";
import { blueText } from "blueText.svelte";
</script>
<target use:redBg use:blueText />
As for interoperability with any regular function. I proposed having lifecycle hooks that allow us to write purely imperative actions like before if necessary.
<!-- regularOldAction.svelte -->
<script>
import { init, destroy } from "svelte/actions"
let node;
init(() => {
console.log("Hi!")
node.innerHTML = "An old action is being used!"
});
destroy(() => {
console.log("Bye!")
});
</script>
<target bind:this={node} />
This would also address some of @isaacHagoel 's concerns. If need be, you can write actions like before. My idea was, indeed, to replace the current actions but the lifecycle hooks would provide an option for purely imperative actions. The mount
and destroy
hooks would be fairly easy to port over from current actions. The current update
function would need to be rethought to use reactive declarations and props. I understand this is more convoluted than the current solution. It would, indeed, be a tradeoff.
As for this:
When we want to implement functionality that can be easily ported to be used with any library/ framework/ vanilla js. Because actions don't need svelte as a dependency they can easily be wrapped with a React hook or be used directly (calling update when needed) in any codebase. I experimented with it here and we do it for one of the internal libraries we've created at work.
I don't have a solution for this issue. An overall would be required. Assuming your use case wouldn't benefit at all from Declarative Actions (I have no idea if it would, pure assumption), and you would still write everything imperatively using the lifecycle hooks, you would need to abstract the functionality away, to a framework agnostic set of functions. Then you could wrap those functions with a react hook, a svelte action, whatever. Instead of: (React Hook (Svelte Action)) you would need to have (React Hook, Svelte Action (FW Agnostic Lib)) This increases Svelte's API surface, which I understand is not desirable.
Keeping old actions around could be an option but deciding between Declarative Actions and Imperative Actions would be a bit of a gray area.
EDIT:
Forgot to address this:
actions in their current form are not limited to reasoning about a single component. they can be used to tie components together (ex: holding a map with references to each component that uses the action and triggering events on any of them whenever it sees fit).
This would only be improved, no? With support for context="module"
, as I alluded to in the RFC, this use case would become trivial.
What do you think?
Direct use of the target
element:
<!-- component.svelte -->
<script>
let target;
let isHeld = false;
</script>
<style>
.red-background {
background-color: red;
}
</style>
<target
class:red-background="{isHeld}"
on:pointerdown="{() => {isHeld = true}}"
on:pointerup="{() => {isHeld = false}}"
bind:this="{target}"
/>
<div class="blue-text" use:target>
I am declaratively styled by an action! yey
</div>
It would work like shortening the entry inside a component, something like the equivalent of this:
<script>
let name = 'world';
let x = ""
let ob = {
style:"color:red;",
"on:click": ()=>{x="click"}
}
</script>
<h1 {...ob}>Hello {name}!</h1>
{x}
But ob
doesn't work and the target
element would work.
Of course, you could create multiple targets in one file.
This could be <svelte:target>
.
This is not meant to overwrite separate files with the action, just to complete them.
Perhaps the target
variable should be exportable.
EDIT: I read that there is something like this mentioned in alternatives, but I don't know if that was exactly what it was about.
@lukaszpolowczyk I don't quite understand what you mean. Would you mind giving another example? How would multiple targets work? When you apply an action to an Element, what would the multiple targets refer to? Actions would still be applied as they are now. Do you want to use target in Components as a "1 child slot"?
@Zizico2 "How would multiple targets work?"
Separate variables - target
and target2
:
<!-- component.svelte -->
<script>
let target;
let target2;
let isHeld = false;
</script>
<style>
.red-background {
background-color: red;
}
</style>
<target
class:red-background="{isHeld}"
on:pointerdown="{() => {isHeld = true}}"
on:pointerup="{() => {isHeld = false}}"
bind:this="{target}"
/>
<target
class:blue-background
on:click={/* function */}
bind:this="{target2}"
/>
<div class="blue-text" use:target>
I am declaratively styled by an action! yey
</div>
<button use:target2>Button1</button>
<button use:target2>Button2</button>
It works like if the code was like this:
<div class="blue-text"
class:red-background="{isHeld}"
on:pointerdown="{() => {isHeld = true}}"
on:pointerup="{() => {isHeld = false}}">
I am declaratively styled by an action! yey
</div>
<button
class:blue-background
on:click={/* function */}>Button1</button>
<button
class:blue-background
on:click={/* function */}>Button2</button>
Basically, the idea is to collect multiple svelte attributes (on:event
class:name
etc.) for an item into one.
Because normal HTML attributes can be done without it - as I showed it from {...ob}
, in the previous comment - with the normal HTML attributes do not have this problem.
The target
and target2
actions can be exported, so this could be used in other components.
This could be a *.svelte
file with only actions.
Then <script context="action">
would not create the action, only the <target>
element itself would create the action, combining all the attributes (functions, variables) and styles.
Now I thought that the problem with init
destroy
could be solved like this:
<script>
let target;
target.init(() => {
console.log("Hi!")
node.innerHTML = "An old action is being used!"
});
target.destroy(() => {
console.log("Bye!")
});
let target2;
target2.init(() => {
console.log("Hi!")
node.innerHTML = "An old action is being used!"
});
target2.destroy(() => {
console.log("Bye!")
});
</script>
Just the <target>
element would have its own life cycle functions, available via bind:this
attribute.
EDIT: Or that?:
<target bind:this={target} lifecycle:init={()=>{}} lifecycle:destroy={()=>{}}>
BTW: Another name could be <svelte:action>
.
@lukaszpolowczyk Ah I see what you mean now. I'll give you my thoughts once I have time to settle down in front of a computer and I might update the RFC with this as an alternative. I quite like it if I'm being honest. But how would you implement arguments? And the current "update" function in actions? Just adding a "lifecycle:update" doesn't sound good as it defeats the point of "Declarative Actions". And using "bind:this" might be a bit misleading, that's what threw me off the first time I read it. "bind:action" or so would be more appropriate. But then how do you get access to the Element the action is applied to? An argument could be passed to the "init" and "destroy" functions. But what if you need to use it outside of those functions? The current way I solved arguments and "update" was to use props and reactive declarations. That's as declarative as it gets and it borrows the system from Components so it would be intuitive to teach. This isn't really possible with multiple actions per file.
@Zizico2 "t. But how would you implement arguments? "I've thought over my idea, and it doesn't make sense. Parameters cannot be used in a meaningful way.
Only your idea makes sense when it comes to a fully declarative solution.
Sorry for the confusion.
Maybe you can combine your idea with the Inline Components idea: https://github.com/sveltejs/rfcs/pull/34
It would look like this:
<svelte:template name="image" context="action" let:image>
{@const isHeld = false;}
<target
class:red-background="{isHeld}"
on:pointerdown="{() => {isHeld = true}}"
on:pointerup="{() => {isHeld = false}}"
bind:this="{target}"
/>
</svelte:template>
<div class="blue-text" use:image={image}>
I am declaratively styled by an action! yey
</div>
What to use:image
is the problem. With Inline Components you use <svelte:component this="image" {image}>
and there is no equivalent for use:action
.
You would have to change name="image"
to e.g. <svelte:template component={Image} let:image>
, where Image is an object-variable. Then it is easy to use use:Image
.
But I don't know if this is a good idea, because it uses <svelte:component this="image "{image}>
for a reason.
A slightly different idea:
You could just extend the standard use:action
:
<script>
let action;
action.init(node, parameters)=>{
return {update:(newParameters)=>{},destroy:()=>{}}
}
<script>
<target
class:blue-background
on:click={/* function */}
bind:action="{target}"
/>
<button use:target>Button1</button>
<button use:target>Button2</button>
But this is not a fully declarative solution.
This is something in between - it is not fully declarative, but it has e.g. on:click
class:name
etc. and there can be many actions in one file (just like the standard use:action
).
But I'm not attached to this idea. Probably combining Declarative Actions with Inline Components would be better.
This would be amazing. A major use case would be when you're using svelte with web components, because styles don't flow down in web components using these type of actions for styling would be very helpful.
First of all a big thumbs up for declarative actions. When I first started using Svelte and wrote my first action I thought "amazing, this is my interface into the regular DOM world and I can do things that are otherwise not possible or much harder with the regular component interface". After writing a couple more actions I realized that I'm feeling the same pain I felt with vanilla js / jQuery: keeping things in sync myself using imperative code. I found myself doing this:
function myAction(node, options) {
// ... actual code ...
function update(newOptions) {
// Is this cheating?
// Svelte reuses the same node for different components, we basically just act like a new action was initialized.
destroy();
myAction(node, newOptions);
}
function destroy() {
// ... undo things ...
}
return {
update,
destroy,
}
}
Because figuring out what to do based on the diff between options
and newOptions
is exactly what I don't want to do manually and it's the job of a computer. I'm tired of simulating a computer in my head (thanks Svelte for doing that for me for the most part).
I think in three years we'll look back and think "why did we ever write imperative actions?"
There are a couple of smaller changes I'd make to this rfc, e.g. <svelte:target>
instead of <target>
and I didn't spend time to read everything it detail. But here's main gist:
What keeps us from compiling such an action-component into the exact same format that actions currently use (with 'svelte/internal'
imports)? All it has to do is use the same invalidation logic it currently does inside update
and magically all operations that use options
are invalidated (current options
argument is literally export const options
). All other things (event listeners, styles) can be compiled to the exact same primitives that already exist, e.g. set_style(node)
. And the logic for cleaning up (e.g. unbinding events, removing classes) already exists in svelte, just put it in destroy
. 100% backwards compatible with the current actions API, but the Svelte way. One of @isaacHagoel concern regarding sharing state between multiple instances is already solved by context="module"
, which would be placed outside of the generated action function.
@Prinzhorn I'm not the right person to comment about your last paragraph, I'm really not that well versed in Svelte's internals.
And I will put <svelte:target>
as an option in the rfc. I chose <target>
because <slot>
isn't <svelte:slot>
either, even though it doesn't behave as vanilla's JS/HTML <slot>
. But I guess the tag does exist so they just used it.
One open question for me is how to make this composable. Right now you can mix and match actions to your liking
function actionA(..) {..}
function actionB(..) {..}
function actionC(..) {
actionA(..);
if (something) {
actionB(..);
}
}
Another open question is how to make a "higher order" action?
function createDragNDrop(..) {
function drag(node) {..} // applied to the drag target
function drop(node) {..} // applied to the drop target
// logic for managing drag and drop zone in here, which can be used by both functions
return { drag, drop }
}
How would these use cases translate to declarative actions?
If you want the goodies of Svelte components, what's stopping us from creating a Svelte component inside an action?
action.js
import Action from './Action.svelte'
export function action(node, {text}) {
const component = new Action({target:node, props: {node,text}});
return {
update: ({text}) => component.$set({text}),
destroy: () => component.$destroy()
}
}
Action.svelte
<script>
export let text;
export let node;
node.style.backgroundColor = 'pink';
</script>
{text}
creating a Svelte component inside an action
And then how do you add on:eventname
, class:name
, transition:fn
etc. from the action
/Action.svelte
level?
The idea is to avoid addEventListener
in action
.
One open question for me is how to make this composable. Right now you can mix and match actions to your liking
If declarative actions are compiled to the same function schema they use now, you could do one of these two:
- Have your "meta" action not be a declarative action (but a "classic" imperative one) and import the two declarative actions and use them as usual (once imported they're just a function).
import actionA from 'actionA.svelte';
import actionB from 'actionB.svelte';
function actionC(..) {
actionA(..);
if (something) {
actionB(..);
}
}
- Allow
use
on<svelte:target>
so you can go full circle and bath in declarativity. So one declarative action can just add more actions usinguse:
. If you want to do that conditionally we end up here https://github.com/sveltejs/svelte/issues/6754
Another open question is how to make a "higher order" action?
See 1. above. Nothing keeps you from re-exporting the declarative action and bundle it with an imperative action.
If you want the goodies of Svelte components, what's stopping us from creating a Svelte component inside an action?
**uses node.style.backgroundColor
to demonstrate goodies** sorry :smile:
How would you suggest we keep both types of actions? A declarative action would be usable with use
or as regular ol' function? Enabling use
on <svelte:target>
is a really clean and succinct solution. I had already answered the composition concerns in an earlier comment with this exact solution. @dummdidumm
Composition should be fairly simple since since
<target />
, or any other Element for that matter, can have multiple actions applied to it.<!-- blueText.svelte --> <style> .blue { color: blue; } </style> <target class:blue />
<!-- redBg.svelte --> <style> .red { background-color: red; } </style> <target class:red />
<!-- composition.svelte --> <script> import { redBg } from "redBg.svelte"; import { blueText } from "blueText.svelte"; </script> <target use:redBg use:blueText />
A declarative action would be usable with
use
or as regular ol' function?
Yes. If you currently import Foo from Foo.svelte
you get a constructor and can do either <Foo>
or manually new Foo()
. If you import a declarative action (a component that has a context="action"
block) doing import foo from foo.svelte
you will get a function (not a constructor) that is indistinguishable from any regular action you wrote imperatively by hand. The module just happens to import from svelte/internal
if needed. (So I'm pretty sure you could compile a declarative action to vanilla JS and use it in React if you really wanted to just like you can with a Svelte component. I don't.)
For me one open question is if this should have a different extension (e.g. .action
). But these are so similar to regular components that it would be weird and tooling like the prettier plugin could already format this right now. The question would be how we teach this if the only difference between declarative action and regular component is the existence of context="action"
? For the compiler it's not an issue, if there is context="action"
it will allow <svelte:target>
and fail if it sees a regular <script>
without context
(like I said above context="module"
would work for actions identical to component right now by scoping it outside the function).
Edit: The big issue with DX/teaching is that from the import statement you cannot distinguish between action and component, but both are used very differently. However, now that I think about it, that's true for literally every .js
file you import. It's your responsibility to actually import a function that corresponds to the action spec, the compiler doesn't know.
Edt2: We can also attach a Symbol
to the current constructor function and to the new action functions to get useful errors during dev. So if you use:
a component or <Foo>
an action, we can detect that.
For the compiler it's not an issue, if there is context="action" it will allow svelte:target and fail if it sees a regular
What about actions without a script at all? Declarative actions would allow for actions that only apply styling or that only wrap the <svelte:target />
element in more elements (should we allow the latter? It's a question I brought to light in the rfc). Should we just force every action to have a script? Even if it is empty?
Edit: The big issue with DX/teaching is that from the import statement you cannot distinguish between action and component, but both are used very differently. However, now that I think about it, that's true for literally every .js file you import. It's your responsibility to actually import a function that corresponds to the action spec, the compiler doesn't know.
This could just come down to a naming convention. UpperCamelCase.svelte
for components and lowerCamelCase.svelte
for actions.
What about actions without a script at all?
Good point, maybe the existence of <svelte:target>
triggers it, because that's what an action requires. It's the point of an action.
I realized that declarative actions would change my mental model for actions.;
- Currently: actions are my escape hatch into the DOM world and I can do with the node whatever I want
- New: actions allow horizontal extension of elements. In contrast to the component system (with composition/context etc.) which are all vertical.
Once I had this new mental model three things came to my mind:
-
"Should we allow more Elements than the
Element in an action?" gets a very clear no from me. That's not what actions are for (that would be a vertical change) and once you allow that it becomes even harder to distinguish components from actions. And it would bring a lot of new problems when a component plus multiple actions want to change the tree (who would win?). With attributes it's just a matter of having a well defined order (e.g. setting an attribute in an action overrides the one in the component). -
Declarative actions would solve another problem I currently have: utility first CSS frameworks. I'm using Tailwind for the first time and it doesn't pair well with Svelte. I don't want to extract things into components just for the styling, for me components need to do something. But even if I extract them things get worse. E.g. I cannot use
animate
because now the element (styled component) is not an immediate child of the keyed each. With declarative actions I could just style my element using actions (with the current actions that would just result in tons of uglitoggleClass
calls). It would also keep things DRY, because currently my styled<Button>
has a path for<a>
and one for<button>
with basically identical styles. -
Maybe we can teach it using the vertical/horizontal terminology?
Regarding the parameters: I think we stick with the current approach, because declarative actions have the current action spec as compile target (they need to be compiled to something anyway). So the parameters become export const parameters
in the declarative component. Doesn't matter what you put in it (a primitive value, function or object) it is reactive in the same way props are right now. If you pass an object and use parameters.foo
somewhere Svelte will do it's surgical updates so that you don't need to do it (that's my main motivation for declarative actions, I want consistency and not diff parameters
and newParameters
in update
manually).
If the maintainers could signal if this is something they can see happening I'll go ahead an review the whole rfc. I still haven't read it in it's entirety.
We shall wait.
And I don't think I understood this:
Regarding the parameters: I think we stick with the current approach, because declarative actions have the current action spec as compile target (they need to be compiled to something anyway). So the parameters become export const parameters in the declarative component. Doesn't matter what you put in it (a primitive value, function or object) it is reactive in the same way props are right now. If you pass an object and use parameters.foo somewhere Svelte will do it's surgical updates so that you don't need to do it (that's my main motivation for declarative actions, I want consistency and not diff parameters and newParameters in update manually).
What's stopping us from allowing multiple props, like in a Component, and then compiling them into a single function parameter? This could be less intuitive then just forcing actions to have a single prop and letting the user handle that themselves I guess, but it would sure be more ergonomic.
What's stopping us from allowing multiple props, like in a Component, and then compiling them into a single function parameter? This could be less intuitive then just forcing actions to have a single prop and letting the user handle that themselves I guess, but it would sure be more ergonomic.
So what you're suggesting is that actions compiled from declarative action always require the argument to be an object? Because right now I have actions that get a single function as argument.
You could always do this if you want to:
<script>
export let parameters;
$: {foo, bar} = parameters;
</script>
This would be 100% backwards compatible. If you you want this:
<script>
export let foo;
export let bar;
</script>
Then you need to change actions that currently don't use an object if you want to migrate to declarative actions.
But there could also be a magic $$params
/ $$restParams
or something that would hold my function and in this case I wouldn't declare any exports?