Description
Describe the problem
I might be missing something but it seems very difficult in Svelte to create components that do nothing more than style basic HTML elements. This makes creating component libraries hard because single element components (buttons, inputs, hr, etc) are the foundation layer of creating a component library. Let's say you want to create a component that does nothing but apply some styles to a button or input. Those styles might also be affected by props. At first it seems trivial:
<script>
export let primary
</script>
<button class:primary ><slot/></button>
<style>
.primary {
background: brown;
color: white;
}
</style>
The problem is the button can't be clicked ... easily fixed:
<button class:primary on:click><slot/></button>
Actually there are a bunch of handlers our users might need. I hope I do not miss any and no new ones get added to the HTML spec:
<button
class:primary
on:click
on:mouseup
on:mousedown
on:mouseenter
on:mouseleave
on:touchdown
on:touchup
on:touchmove
on:pointerdown
on:pointerup
on:pointermove
on:focus
on:blur
on:keydown
on:keyup
on:transitionstart
on:transitionend
><slot/></button>
Now we have added these handlers the compiled output to my component is massive. It shows I am creating a bunch of listeners that most of my consumers won't use so this is using up memory for each instance. So Svelte is no longer feeling very svelte:
if (!mounted) {
dispose = [
listen(button, "click", /*click_handler*/ ctx[4]),
listen(button, "mouseup", /*mouseup_handler*/ ctx[5]),
listen(button, "mousedown", /*mousedown_handler*/ ctx[6]),
listen(button, "mouseenter", /*mouseenter_handler*/ ctx[7]),
listen(button, "mouseleave", /*mouseleave_handler*/ ctx[8]),
listen(button, "touchdown", /*touchdown_handler*/ ctx[9]),
listen(button, "touchup", /*touchup_handler*/ ctx[10]),
listen(button, "touchmove", /*touchmove_handler*/ ctx[11]),
listen(button, "pointerdown", /*pointerdown_handler*/ ctx[12]),
listen(button, "pointerup", /*pointerup_handler*/ ctx[13]),
listen(button, "pointermove", /*pointermove_handler*/ ctx[14]),
listen(button, "focus", /*focus_handler*/ ctx[15]),
listen(button, "blur", /*blur_handler*/ ctx[16]),
listen(button, "keydown", /*keydown_handler*/ ctx[17]),
listen(button, "keyup", /*keyup_handler*/ ctx[18]),
listen(button, "transitionstart", /*transitionstart_handler*/ ctx[19]),
listen(button, "transitionend", /*transitionend_handler*/ ctx[20])
];
...
Then we need things like ids and data attributes ... but then there is that warning in the docs that spreading props is not ideal and can't be optimized:
<button {...{$$props}} on: ... ...>Click me!</button>
So now I have a deoptimized component and all the cool stuff in Svelte does not work on my Button
<Button use:proximityFetch transition:fade><slot/></Button>
Error:
Transitions can only be applied to DOM elements, not components
Actions can only be applied to DOM elements, not components
For inputs binding does not work if you do a props spread:
<Input bind:{value} />
I just wanted to make the button brown when it was primary and I had to do a ton of boilerplate for every event an element might receive and I lost a lot of functionality (use, transition). It means creating a styled button is for advanced users not beginners. Styled buttons, links, inputs is the bread and butter of building websites and it feels hard to do this well in Svelte.
At the moment because single elements wrapped in components lose Svelte powers I find myself reapplying utility classes to basic elements rather than consolidating style logic into reusable atoms in a component library.
Describe the proposed solution
It would be great to have a special thing
that was just for styled elements. I'd be happy if all it could do was modify style
and class
props and perhaps enforce a type
for input
. All other props, event handlers, actions, bindings get automatically forwarded to the element. I.e. it allows users to create things that have the same API as Svelte elements not Svelte components. Think of them as middleware or a proxy. They may need a different file extension or compiler options. They might look like this but I am open to other ways of achieving the same thing:
<script>
export let primary
export let x
</script>
<svelte:element
default='button'
style={style => `tranform: translate3d(${x}px,0,0);${style};`}
class:primary
/>
<style>
.primary {
background: brown;
color: white;
}
</style>
It can then be used to apply styling logic to an element
import {Button} from "./Button.svelte"
import {foo} from "./foo.js"
<Button on:mouseenter={console.log} use:foo />Click</Button>
Nice to have but not essential
An as
prop:
<Button as="a" href="/">Ok</Button>
There might be an allow list and disallow list for element types.
Alternatives considered
In some cases allowing multiple supporting elements would be handy e.g. checkboxes are often wrapped in a label however once we allow multiple elements we do not know which element to send which forwarded which attributes to. Typically you would want values to go to the hidden input and animations and transitions to be forwarded to the container. So it seems best to limit this to single element components only.
Another alternative might be middleware components, they do not render anything but parents can manipulate the props, handlers and styles of their children until finally the props, handlers and styles reach a single element. So the source of an Input elementComponent
might look like this:
<focusStyles>
<inputStyles>
<eventLogger>
<forceType type="number">
<input />
<forceType>
<eventLogger>
</inputStyles>
</focusStyles>
This requires two new kinds of components elementComponents
and propMiddleware
components.
Importance
would make my life easier