-
-
Notifications
You must be signed in to change notification settings - Fork 4.5k
Let the community create custom runes that offer similar ergonomics as Svelte 5's native runes #11014
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
Comments
Here is the implementation of your code using destructured $derived.by (ignore |
I think it should be done by making a third party package a vite plugin. Similar to what dark runes does. The key difference is that it shouldn’t be a hack around the compiler but a supported extension with convenience APIs. At least plugin authors should get access to the AST. Maybe some inspiration could be taken from Rust macros |
But then, won't the Svelte language server that powers the VSCode plugin for Svelte constantly complain about runes it doesn't recognize? |
If this were to be officially supported then plugins could register the new runes with svelte which would tell the language server how to handle them. |
If I understand your message correctly then I'm all for it. So you mean that 3rd party libraries would need to write their own vite plugins that would, similar to Svelte's own compiler, compile custom runes into more normal javascript? And then all you're asking is for Svelte's vite plugin to offer a way to register our custom runes so that it can let the language server know about our newly created custom runes and how to interpret them? Did I understand you correctly? If yes then I'm all for it. I don't mind having to write a plugin / pre-compiler for Vite. As long as I get the ergonomics of native runes in the end. Though I imagine that if the maintainers of Svelte would want to allow / support this, that they might want to enforce some rules about how these custom runes are allowed to function etc. And if that's the case then I do think a tighter integration of this feature in Svelte's own compiler will be necessary. |
This is a topic that the maintainers have discussed intensively, and thus far we've come down on the side of not doing it. The design outlined here imposes a lot of constraints (naming conventions, a single For example: we haven't implemented state-based replacements for the <script>
import { spring } from 'svelte/motion';
const progress = spring(0);
</script>
<progress value={progress.current} />
<button
onclick={() => {
// we want `progress.target` to be read/write, but `progress.current` should be readonly
progress.target += 0.1;
}}
>next</button> If this were a By just using getters and setters we're able to have all this flexibility, and we don't need to make the framework itself more complicated. Personally I would choose this... <script>
import { activeElement } from 'svelte-use';
</script>
<p>{activeElement.current?.nodeName ?? 'no active element'}</p> ...over this: <script>
import { $useActiveElement } from 'svelte-use';
let activeElement = $useActiveElement();
</script>
<p>{activeElement?.nodeName ?? 'no active element'}</p> There's less conceptual overload, less code, and it better reflects the singleton nature of |
When you say:
Why do we want that? I can't think of a time when I've wanted that, and I use I'd assume the simplest, most intuitive api would just be <progress value={progress.value} />
<button
onclick={() => {
progress.value += 0.1;
}}
>next</button> |
That's how the stores work today, yeah. But I think it's weird if you read a value that you just assigned and it's something different: progress.value += 0.1;
console.log(progress.value); // still the previous value because nothing has updated yet! And if I hammer the '+1' button repeatedly, I want the target value to increment by 0.1 — in other words if I press it 10 times, the value should eventually end up at exactly 1. But if I do Treating |
Describe the problem
Both Solid and Vue are known for also using (their own version of) signals as their reactive primitives. Now of course, their signals aren't as ergonomic as Svelte 5's signals, because in Vue one has to access a ref's underlying value with
.value
, and Solid requires one to use function calls. As opposed to Svelte 5, which offers the possibility to simply treat the variables as if they were simply the raw values that one put into$state()
.However, the fact that both Vue and Solid give the developer direct access to the signals they create, allows those developers to create entire libraries full of extra reactive primitives, such as VueUse and Solid Primitives, which offer the exact same usage API as Vue's and Solid's native signal implementations.
Svelte 5, in its current form, does not allow this. There's no way to create a
let activeElement = $activeElement()
rune for example, that allows one to useactiveElement
like a plain variable such as$state()
would offer. (Aside from the detail that you're not allowed to prefix anything with a$
sign anymore, but that's less important.) More importantly, you would always need to design it so that the consumer of your library would always need to useactiveElement.value
oractiveElement()
to access and / or mutate the underlying reactive value.I would deeply appreciate if the maintainers of this awesome framework / compiler called Svelte would be willing to offer some way for us developers to return our own reactive primitives / runes that are treated equally to
$state
and$derived
. So that we can build libraries full of great reactive utilities like VueUse and Solid Primitives.Describe the proposed solution
I propose we expand the limitations on the
$
prefix a little bit. I understand why you've chosen to disallow new functions with$
prefixes, because of course you might want to add more native runes in the future. So how about normal functions are still not allowed to be prefixed with$
, with two exceptions. A developer is allowed to prefix a function name with$use
, which would signify that this function produces a writable rune similar to$state
. And a developer is allowed to prefix a function with$get
to signify that this function produces a read-only rune, similar to$derived
. (Of course this should also work for the names of arrow-function-constants.) But other than those two prefixes, all other names that start with$
will still be disallowed, to protect Svelte's maintainers' ability to add new native runes at any point in the future.So
let activeElement = $activeElement()
would becomelet activeElement = $useActiveElement()
. And we make sure that the compiler understands that thenactiveElement
should be treated the same as if it were created using$state()
. For this, of course, any function created with the$use
and$get
prefix should adhere to a certain contract, so that the compiler will later know how to get and / or mutate the underlying reactive state.I'm not attached to any specific contract, but one example could be that the compiler enforces the function to always return an object with
.value
getters and setters:So reading
activeElement
anywhere, whether in the javascript code or in the template, will turnactiveElement
intoactiveElement.value
in the code generated by the compiler.So this code:
Would compile into this code:
And this code:
Would compile to this, just like how
$derived
works:Then on the inside of
$useActiveElement()
, it should be assumed that if there's ever any argument passed in, it will always be a single argument at most (enforced by the compiler) and it will always be a function that should be treated as a getter function.The final code could look something like this:
As you can see, I'm defining this function in a file called
active-element.svelte.ts
, which is necessary by definition, because I will always need to use$state
under the hood in such a function. Which adds another bonus to the contract. You could make the compiler only treat functions prefixed with$use
/$get
as custom runes if and only if they were imported from a file which includes.svelte
in the filename. This ensures that this naming convention for custom runes does not collide with any 3rd party libraries that did not intend for their functions to be interpreted as custom runes.Alternative solutions
Never mind, I proposed an alternative solution, but now I actually don't think this alternative is good enough to even consider. But if you really want to read it then you can expand this thing to read it.
We could also try something similar to what Vue tried once. Although since it didn't work for them, I'm not sure if it would work for us. But what if we would allow developers to "runify" any function that returns an object with a
.value
getter / setter, by wrapping the function call into a$()
.So let's rename our
$_activeElement
touseActiveElement
and then in a component it could be used like so:This way I can easily think of a way to distinguish between a writable and read-only rune. For example, like so:
Edit
Hmm, you know what, I don't think
$.writable(useActiveElement())
is a good idea. Because that would mean that the consumer of a function suddenly decides whether to treat the output of the function as readable or writable. Which is of course wrong, because the author of the function will already have decided whether to make their output readable or writable (depending on whether or not they offered a.value
setter function of course).Importance
very nice to have
Edit
I just changed the naming convention to
$get
prefixes for read-only custom runes and$use
for writable custom runes. That looks clean to me and is also descriptive enough that I imagine people will find it very easy to learn.The text was updated successfully, but these errors were encountered: