Skip to content
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

$ref() rune for using directives on components #14831

Open
ottomated opened this issue Dec 24, 2024 · 7 comments
Open

$ref() rune for using directives on components #14831

ottomated opened this issue Dec 24, 2024 · 7 comments

Comments

@ottomated
Copy link
Contributor

Describe the problem

A commonly requested feature (#12229, #2870, #5236) is to have parity between native elements and svelte components for directives such as class:, style:, use: or transition:. This currently errors:

<Component style:color="red" />
<!-- This type of directive is not valid on components -->

Why is this bad? It breaks intuition for new developers who expect components to behave the same as elements. It makes wrapper components inherently more limited than native elements.

Several points of implementation difficulty have been brought up in the past:

  1. How do you deal with scoped CSS classes?
  2. Which element inside the component do you apply the directives to?
  3. What if multiple elements in a component need to receive the directive?

Describe the proposed solution

The $ref rune would provide a way for components to determine which element receives the directives.

<script>
  const props = $props();
  let ref = $ref();
</script>

<button bind:this={ref} {...props}></button>

Any component that does not explicitly use $ref() still throws an error when a directive is used on it.

I believe this answers the main issues on this topic.

  1. How do you deal with scoped CSS classes?

Right now, if you pass a class prop to a component, it uses the scoped css hash from that component (playground). class: would behave the same way, i.e. <Comp class:foo /> would use the .foo defined in Comp.svelte.

  1. Which element inside the component do you apply the directives to?

This proposal leaves the choice to the component creator. They could specify which of the root elements receives the directives, or a non-root element, or a child component like this:

<!-- Parent.svelte -->
<Child use:action />

<!-- Child.svelte -->
<script>
  const ref = $ref();
</script>
<Grandchild bind:this={ref} />

<!-- Grandchild.svelte -->
<script>
  const ref = $ref();
</script>
<div bind:this={ref} />
<!-- this element receives the action -->
  1. What if multiple elements in a component need to receive the directive?

In this proposal, it would be impossible. However, here are some alternatives:

Alternatives

  1. Use the rune only inside the property:
<div bind:this={$ref()}></div>

This doesn't allow using the bind:this for a local use, though.

  1. Don't use bind:this
<script>
  let canvas;
  const ref = $ref();
</script>
<canvas bind:this={canvas} {ref}></canvas>

Could also be svelte:ref={ref} or something.

  1. Allow multiple $ref()s per component
<script>
  let ref1 = $ref();
  let ref2 = $ref();
</script>
<div bind:this={ref1}></div>
<div bind:this={ref2}></div>

In this case, the directives would be duplicated and both elements would receive them.

I'm interested in knowing if there's some other barrier that makes this impossible, but I haven't found one while researching this issue!

Importance

would make my life easier

@webJose
Copy link
Contributor

webJose commented Dec 24, 2024

Essentially a duplicate of #9299.

@levibassey
Copy link

levibassey commented Dec 24, 2024

IMO they should all just be props. Look at the improvements to the class attribute for example, it now accepts an object on which you can specify conditions for your classnames. This makes the class: directive redundant and even better, you can pass the object in as a prop

So I believe the same thing should to be done with the style: directive
For example this,

<button
    class="card"
    style:transform={flipped ? 'rotateY(0)' : ''}
    style:--bg-1="palegoldenrod"
    style:--bg-2="black"
    style:--bg-3="goldenrod"
    onclick={() => flipped = !flipped}
>

becomes,

<button
    class="card"
    style={{
        transform: flipped ? 'rotateY(0)' : "",
        "--bg-1": "palegoldenrod",
        "--bg-2": "black",
        "--bg-3": "goldenrod"
    }}
    onclick={() => flipped = !flipped}
>

the latter will work on both elements and components. I imagine something similar will be done with the other directives

@ottomated
Copy link
Contributor Author

IMO they should all just be props

@levibassey how do you envision the use: directive working as a prop?

<div use={{
  action: () => {}
}} />

feels very clunky.

I guess transition: would be ok as this:

<div transition={fly({ x: -100 })} />

@ottomated
Copy link
Contributor Author

Essentially a duplicate of #9299.

Similar, sure. I'm not proposing that a Component with a $ref defined presents its own bind:this as its ref, though I guess it could. I also don't think that there should be any automatic assumptions if there's only one root element - it should be explicit.

@levibassey
Copy link

levibassey commented Dec 24, 2024

@levibassey how do you envision the use: directive working as a prop?

honestly, I don't know, the use: directive is a tricky one, but I heard from a few peeps in the svelte discord that the team has been discussing ways to pass in actions and transitions as props.

Rich also said something about it a few months back

@Leonidaz
Copy link

Leonidaz commented Dec 28, 2024

Btw, there is a proposal to make style take an object: #14832

I also think that actions and transitions should be passed in as basically props vs having to rely to $ref. As a rule the component should know which props should be used on which elements and which should be passed further down to other components.

Maybe introduce more runes like $action and $transition and only allow one of each per component, same as with elements? EDIT: Can be done without runes with regular props since the syntax on html elements for transitions and actions would already be clues to the complier due to its special syntax.

<!--Parent.svelte usage-->
<!--action / transition props could be anything that Child expects-->
<Child
  action={{runner: (node, data) => {/*action body*/}, data: {}}}
  transition={{animator: fly, options:{ y: 200, duration: 2000 }}} />

<!-- or nothing is passed in to the Child and svelte doesn't apply transitions / actions -->
<Child />

<!--Child.svelte-->
<script>
   let { action, in, out, onintrostart, onintroend, onoutrostart, onoutrostart, otherProp } = $props();
</script>

// various example of possible usage:

// use the options for transitions provided by the parent
<div transition:{transition?.animator}={transition?.options} {onintrostart} {onintroend} {onoutrostart} {onoutrostart}>Something</div>

// customized usage to override transition options provided by the parent
<div transition:{transition?.animator}={{...transition?.options, duration: 1000}}>Something</div>

// in / out props
<div in:{in?.animator}={{...in?.options, duration: 500}} out:{out?.animator}={out?.options}>Something</div>

// use of action with parent provided params
<div use:{action?.runner}={action?.data}>Hello there</div>

// use of action with own params:
<div use:{action?.runner}={() => signal}>Hello there</div>

@Leonidaz
Copy link

Simplified and updated the example from yesterday for using transitions and actions as props. Here's the link to the previous comment: #14831 (comment)

It would be just like using any regular props and would allow dynamically changing actions with data or transitions with options.

Svelte can also easily account if the passed in props are nullish (not passed in or removed later) and not apply actions / transitions. So, components can declare their usage via transition: or use: but the parents don't have to provide them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants