Skip to content

Styling single element components while retaining element API #6882

Open
@philholden

Description

@philholden

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions