Description
Describe the problem
Svelte handles composability on the logic level quite well, with first-class support for stores. But composability on the HTML element level is a little bit lacking. The use:action
feature is great for adding composable features, but to define an action you just write a function, without the benefit of any sveltiness. But things like classes and event listeners are inherently composable, so it's a shame that it's not simple to make use of that in an ergonomic way.
Along with this, IMO we think too quickly of reusable components instead of composition. For an example, consider the Svelte Material UI library (not trying to pick on this, it just illustrates the point). If you look at the svelte component for a MaterialButton
, you'll see all the hallmarks of a component that should be a composable; things like:
<svelte:component this={component}
use={[/* ..., */ forwardEvents, ...use ]}
- The dreaded
{...$$restProps}
{href}
(just in case the consumer wants to use a link, etc.)
In summary, the author of this reusable component had to consider: what are all the ways a consumer of this component might conceivably want to use it? I need to make sure I make the underlying semantic element dynamic, pass down all the correct attributes and forward all the expected events, merge all the internal and external classes, etc etc. This should not be.
Describe the proposed solution
I would like the ability to write a svelte composable in a similar way to the way we write svelte components, with full compiler support, that compiles to an Action function instead of a Component. For type reasons, it would probably have to be a separate file extension, something like .svelte-composable
(way too long, I know). In composable files, I would envision that we can have <script>
, <script context="module">
, and <style>
blocks as normal, but instead of other HTML elements we would have a single <svelte:composable>
element. Here's a simplified example of the code I would want to write when defining something like a material button:
<!-- ripple.svelte-composable -->
<script>
function handleClick(event) {
// Do the ripple
}
</script>
<svelte:composable class="ripple-container" on:click={handleClick} />
<style>
.ripple-container {
/* ... */
}
</style>
<!-- button.svelte-composable -->
<script>
import ripple from "./ripple.svelte-composable";
</script>
<svelte:composable class="material-button" use:ripple />
<style>
.material-button {
/* ... */
}
</style>
<!-- raisedButton.svelte-composable -->
<script>
import button from "./button.svelte-composable";
</script>
<svelte:composable class="material-button--raised" use:button />
<style>
.material-button--raised {
/* ... */
}
</style>
<!-- userland.svelte -->
<script>
import raisedButton from "some-material-lib/raisedButton.svelte-composable";
</script>
<button use:raisedButton>
I'm a button
</button>
<a use:raisedButton href="#">
I'm a link
</a>
Alternatives considered
No alternatives that I've personally considered, but there's a lot of considerations that would be non-trivial. Here's a few:
- How would style scoping be handled?
- Could a
<svelte:composable>
element have children? What exactly would that mean (order of insertion, etc)? - How difficult would it be to update the compiler to handle this?
Importance
would make my life easier