Svelte: Allow indicating component "root" element so actions may be used on the component tag

Created on 31 Jul 2020  Â·  6Comments  Â·  Source: sveltejs/svelte

Is your feature request related to a problem? Please describe.

I cannot augment third-party components with actions because components in Svelte do not have an implicit host/root element at runtime. The most simple example I can think of is a button component from a library that does not support tooltips, but I would like to add a tooltip to the button.

<script>
    import MatButton from "svelte-material";
    import myTooltip from "../actions/tooltip";
</script>

<!-- Obviously, tooltips are not as relevant for text buttons but I am simplifying the example -->
<MatButton use:myTooltip={"Click me!"}>
    Login
</MatButton>

The above code does not compile because there is no host element at runtime for the MatButton so there is no target for the action. Thus, the MatButton component must support a tooltip property or I'm out of luck.

Describe the solution you'd like

I would like components to be able to optionally designate an element in their markup as the root/host element so that actions used on instances of the component get forwarded to that element. In my example, MatButton.svelte might look something like this:

<script>
    // (omitted)
</script>

<!-- Notice this button is marked as the host -->
<button svelte:host>
    <slot/>
    <div class="ripple"></div>
</button>

Because the MatButton component gets replaced by a single button element, I think it is very intuitive for users of the library to think of the MatButton and the button element it gets replaced by as one and the same.

At first, I thought the smartest solution would be to automatically allow actions on a component provided its markup has exactly one root element. However, I realized there might be situations where a component has multiple root elements but only one is visible at runtime and is the 'primary' element while the others are there for intercepting focus or some other shenanigans, etc. Besides, it's probably better for component authors to be explicit about whether or not their component maps to a specific runtime element. Thus, I arrived at the svelte:host marker attribute.

Describe alternatives you've considered

  1. Implicitly creating a runtime host element for every component at runtime and injecting components' markup into their host elements. Just kidding! I think Svelte's approach where it replaces component instances with the component markup is vastly superior to Angular and the other frameworks. It gives the developer more control over what the DOM structure looks like at runtime—which means better performance and fewer CSS headaches, and also allows the developer to create very powerful recursive components. Fun Fact: Angular ended up having to work around this madness via attribute components (like <button mat-button> instead of <mat-button>) so that the resulting DOM could be less convoluted and more semantic.

  2. For my simple tooltip example, I could create a TooltipHitbox component with a <slot/> inside a <div use:myTooltip={tooltipProp}> and then wrap MatButton instances with that component. This would create unnecessary wrapper elements at runtime, potentially causing issues with styling, and is also needlessly verbose and obnoxious.

How important is this feature to you?

This feature is not a dealbreaker for me as I feel it is the only bad tradeoff to Svelte's replace-with-markup approach for components. That said, it does make third-party components less extensible because you cannot use actions on them and you cannot forward stuff to their internal markup within your own templates. This means you either have to use a jank workaround (see my first alternative solution) or you end up writing your own version of a library component just because you need to apply an action to the rendered element.

pending clarification

Most helpful comment

I don't think we necessary _need_ to be able to specify a root element(s). I'm not necessarily opposed to the idea; just wanted to point out the possibility that it _could_ still be useful to have actions that work for components even if the action function only had a reference to the component and not to any root element(s) (see use case below).

(The main reason I _would_ want the ability to specify root element(s) would be for easily applying CSS classes to the root element(s) of child components (this would address https://github.com/sveltejs/svelte/pull/2888#issuecomment-554681896, https://github.com/sveltejs/svelte/pull/2888#issuecomment-554758932, etc.) )

  1. If we _did_ add that ability, I'd want to be able to specify multiple "root elements" and have the specified actions automatically applied to _each_ of them.

  2. Don't forget that there could also be 0 root elements (like if the child component's template had nothing but a <slot />).

  3. I really do wish there was a way to make actions that could apply to components.

I realize that the current definition of an action is in terms of an _element_, but I think it could be changed to work for components too.

This would let you do things like:

  • attach event handlers (on:whatever) to the _component_ (from the action function), and have it work exactly the same as if you had passed on:whatever={handler} directly to the component that is the _subject_ of this action/trait/enhancer/smoosher.
  • define other things to happen on component lifecycle events (onMount, onUpdate) (I don't have any examples of what I'd use this for; just brainstorming)

In particular, what I want to do is something like this (a use:attachEvents={eventProps} action) — but for components. I just don't know if there's a programmatic way to attach event handlers to (there must be one internally, but probably not one that's exposed currently). In other words, if all we had a reference to is the component instance, how would you translate/adapt this:

node.addEventListener(e, f)

to programmatically (with JS rather than via the template syntax) add an event listener to a _component_?


They don't need to add a prop for every action. The action itself can be passed in as a prop.

That's awesome that you can do that (I didn't realize that), but the main problem with that approach is that _it only works if you own the component_ that you want to add your behavior/action to. How do you add an action to a component if you _don't_ own the component that you want to add a behavior/action to (that is, if you import it from a library)? (Attaching event handlers is the main use case I have for this.)

There needs to be a way to affect child components _without their cooperation_ (as @syntheticore aply put it). Well, you already _can_ attach those event handlers to a child component without their cooperation if you explicitly list them out every time (so it's not like I'm proposing some new way to break encapsulation and give you more control over something _inside_ your child component). This is more about bundling some behavior together into a reusable function, letting you create reusable behaviors/hooks/actions and _avoid duplicating the code_ that provides that behavior/pattern (by explicitly listing out the same list of event handlers every time you want to reuse this pattern).

This is an area where React really has an abundant supply of features/abstractions to allow reusability (HOCs, hooks, spreading props that may include event handlers (since they are simply props (onChange) like any other prop)) and Svelte feels like it is lacking... I want React hooks — in Svelte. (To clarify: React doesn't actually provide a way to apply hooks to _child_ components (you use hooks in your _own_ component), so that's kind of a bad example/analogy, but they do provide a nice way to bundle reusable behavior into a function, kind of like Svelte actions, so they're still the closest analogue I can think of to Svelte actions in React. My point is I do somehow feel like that powerful feeling that you can extract literally just about _anything_ from your specific component into some generic reusable construct — be it a component (which is the main solution to reusability that Svelte provides), or a hook (this seems missing in Svelte), or a HOC (which _might_ eventually be possible in Svelte through inline components, though probably not since they will probably only be possible from within a .svelte template and not from within a function in any old .js file) — is somehow missing, coming to Svelte from React...)

Sorry, I should probably start a new issue/proposal for this (since the OP's issue/proposal is specifically about applying actions to a component's root _element_(s))...

All 6 comments

If I understand this correctly - if you control the markup of the child component, you can easily add an action to it which is passed as a prop, so this doesn't really change anything other than adding API surface for something which, should the author of the child/third-party component wish, they could expose an API for anyway.

@antony Yes, I understand that. The point of the feature is to not rely on the third-party author of the child component to add a prop for every action under the sun. Rather, they could just mark a recipient for actions on the component (assuming there is a viable target element), and then consumers of the library could extend the component using whatever actions they desire.

Relying on the component author to implement a prop for every desired action is not decentralized at all as it means they must bake every feature directly into the library. This has already forced me to forgo Svelte Material because I would like to add some actions to their components but I cannot and it does not make sense for them to cater to my specific use-case by baking random stuff into the library used by everyone.

They don't need to add a prop for every action. The action itself can be passed in as a prop.

<script>
  export let action;
</script>

<div use:action>whatever</div>

The argument for the action can be another prop or can be part of the same prop.

Okay, that is interesting. It makes sense since an action is just a function that could be passed as a value anywhere. So let's say I want to apply multiple actions. I could (presumably) write the following:

multi-action.action.ts

type ActionFn = (target: HTMLElement, opts?: any) => undefined | ActionCallbacks;

interface ActionCallbacks {
    update?: (opts?: any) => void;
    destroy?: () => void;
}

/**
 * Simple action which runs multiple actions on an element.
 */
export default function multiAction(actions: [ActionFn, any][]): ActionFn {
    return function(target: HTMLElement): ActionCallbacks {
        const handles = actions.map(([fn, opts]) => fn(target, opts));

        return {
            destroy(): void {
                for (const handle of handles) {
                    handle?.destroy?.()
                }
            }
        };
    }
}

MatButton.svelte (from third-party library)

<script>
    export let action;
</script>

<button use:action>
    <slot/>
</button>

MyComponent.svelte

<script>
    import MatButton from "@smui/button";
    import multiAction from "../actions/multi-action.action.ts";
    import myTooltip from "../actions/tooltip.action.ts";
    import readOnHover from "../actions/read-on-hover.action.ts";
</script>

<!-- Where readOnHoverOpts is some options object which might change reactively (not shown here) -->
<MatButton action={multiAction([[myTooltip, "Click me!"], [readOnHover, readOnHoverOpts]])}>
    I'm a cool looking button
</MatButton>

I'm assuming the above would be less than ideal performance-wise. If my intuition is correct, whenever readOnHoverOpts changes in MyComponent.svelte, Svelte will re-evaluate the multiAction(...) call and pass the new result to the action prop of MatButton.svelte. This will cause the previous multiAction instance to be destroyed and the new one to be executed with the <button> element.

I have read through the API docs and nowhere did I see a mention of a way to apply an array of actions to an element. Thus, MatButton.svelte _could_ provide action1, action2, etc. props but that is not really a solution. So with MatButton.svelte providing only an action prop, I am forced to use my multiAction function which does not support updating action parameters without destroying the existing set of actions and recreating them completely (once again, assuming my intuition is correct).

I also don't like that this solution is not really visible to the Svelte compiler whatsoever and maybe misses out on some optimizations? Like if the Svelte compiler knew you were applying 3 particular actions it could generate code to apply and update each of those actions individually vs. just having a runtime loop over an array of actions and having to check if any of the actions in the array were changed to a different function entirely.

The more I think about it the more I recognize this is a hard problem. It just seems like a proper method to apply actions to a component's internal markup from another component's markup would be much more optimizable and hygienic from an app-developer standpoint. That said, I don't know much about the Svelte compiler implementation and my tingly feelings tell me this would be a lot of work to implement. I am very grateful for Svelte as it is now so I am really just looking for people's thoughts on this particular problem. Maybe forwarding an action prop (in combination with something like multiAction if multiple are needed) is by far the sanest approach. Please let me know your opinion given my concrete examples.

I don't think we necessary _need_ to be able to specify a root element(s). I'm not necessarily opposed to the idea; just wanted to point out the possibility that it _could_ still be useful to have actions that work for components even if the action function only had a reference to the component and not to any root element(s) (see use case below).

(The main reason I _would_ want the ability to specify root element(s) would be for easily applying CSS classes to the root element(s) of child components (this would address https://github.com/sveltejs/svelte/pull/2888#issuecomment-554681896, https://github.com/sveltejs/svelte/pull/2888#issuecomment-554758932, etc.) )

  1. If we _did_ add that ability, I'd want to be able to specify multiple "root elements" and have the specified actions automatically applied to _each_ of them.

  2. Don't forget that there could also be 0 root elements (like if the child component's template had nothing but a <slot />).

  3. I really do wish there was a way to make actions that could apply to components.

I realize that the current definition of an action is in terms of an _element_, but I think it could be changed to work for components too.

This would let you do things like:

  • attach event handlers (on:whatever) to the _component_ (from the action function), and have it work exactly the same as if you had passed on:whatever={handler} directly to the component that is the _subject_ of this action/trait/enhancer/smoosher.
  • define other things to happen on component lifecycle events (onMount, onUpdate) (I don't have any examples of what I'd use this for; just brainstorming)

In particular, what I want to do is something like this (a use:attachEvents={eventProps} action) — but for components. I just don't know if there's a programmatic way to attach event handlers to (there must be one internally, but probably not one that's exposed currently). In other words, if all we had a reference to is the component instance, how would you translate/adapt this:

node.addEventListener(e, f)

to programmatically (with JS rather than via the template syntax) add an event listener to a _component_?


They don't need to add a prop for every action. The action itself can be passed in as a prop.

That's awesome that you can do that (I didn't realize that), but the main problem with that approach is that _it only works if you own the component_ that you want to add your behavior/action to. How do you add an action to a component if you _don't_ own the component that you want to add a behavior/action to (that is, if you import it from a library)? (Attaching event handlers is the main use case I have for this.)

There needs to be a way to affect child components _without their cooperation_ (as @syntheticore aply put it). Well, you already _can_ attach those event handlers to a child component without their cooperation if you explicitly list them out every time (so it's not like I'm proposing some new way to break encapsulation and give you more control over something _inside_ your child component). This is more about bundling some behavior together into a reusable function, letting you create reusable behaviors/hooks/actions and _avoid duplicating the code_ that provides that behavior/pattern (by explicitly listing out the same list of event handlers every time you want to reuse this pattern).

This is an area where React really has an abundant supply of features/abstractions to allow reusability (HOCs, hooks, spreading props that may include event handlers (since they are simply props (onChange) like any other prop)) and Svelte feels like it is lacking... I want React hooks — in Svelte. (To clarify: React doesn't actually provide a way to apply hooks to _child_ components (you use hooks in your _own_ component), so that's kind of a bad example/analogy, but they do provide a nice way to bundle reusable behavior into a function, kind of like Svelte actions, so they're still the closest analogue I can think of to Svelte actions in React. My point is I do somehow feel like that powerful feeling that you can extract literally just about _anything_ from your specific component into some generic reusable construct — be it a component (which is the main solution to reusability that Svelte provides), or a hook (this seems missing in Svelte), or a HOC (which _might_ eventually be possible in Svelte through inline components, though probably not since they will probably only be possible from within a .svelte template and not from within a function in any old .js file) — is somehow missing, coming to Svelte from React...)

Sorry, I should probably start a new issue/proposal for this (since the OP's issue/proposal is specifically about applying actions to a component's root _element_(s))...

I could see that access to an elements attributes/properties etc could be considered a corollary to the notion of a slot.
Where a slot allows IOC for element contents, similar IOC systems could make sense for the elements attributes/properties/actions etc

Similar to slots, it would make sense to have both named and unnamed "element" slots.

Some imagined use cases for this is in complex elements such as input's where there are myriad properties and implementing logic for all of them in a component would have a large overhead but little benefit.
If instead the components input control had an "element" slot, the component user could easily extend the functionality in a meaningful and relatively safe way.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rob-balfre picture rob-balfre  Â·  3Comments

matt3224 picture matt3224  Â·  3Comments

1u0n picture 1u0n  Â·  3Comments

angelozehr picture angelozehr  Â·  3Comments

noypiscripter picture noypiscripter  Â·  3Comments