-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
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
Allow indicating component "root" element so actions may be used on the component tag #5218
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 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, 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 |
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 #2888 (comment), #2888 (comment), etc.) )
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:
In particular, what I want to do is something like this (a node.addEventListener(e, f) to programmatically (with JS rather than via the template syntax) add an event listener to a component?
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 ( 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. 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. |
@intelcentre I like the way you're putting it. @TylerRick I half-agree with what you're saying but I would also like the Svelte team to feature-gate very strictly because an influx of features is a surefire way to end up with another React or Angular and I am NOT saying that with a positive connotation. @Conduitry I have been working on a large Angular frontend for years now, and the more I work with component libraries like Angular Material and watch them try to add every feature under the sun to accommodate the myriad use cases developers, the less I like the library because I don't need any of that junk and it just adds overhead both in terms of bundle size and documentation. Thus, I have come to the conclusion that because user interfaces are so diverse, the best answer is often to just reinvent the wheel and make your own components from scratch which tailor to your own use case--and I believe Rich Harris mentioned the same belief in one of his talks. That said, rewriting components which are 90% similar all the time is a very pessimistic, conservative approach. Does the Svelte team have any ideas about how to make components more extensible without bloating Svelte? The only way I could think of is a built-in method to expose the root elements of a component to library users, but I now realize I could already do this by binding internal component elements to exported variables and then component users can bind the component instance and access those variables. Please offer your thoughts and close this issue if it is a dead-end. 😃 |
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
I'd love to have this so I could add a class to a component, to for instance add some margin |
I've built a fair number of library components at this point and the For instance, say I want to make a Form component that has a specific way of managing state and communicating with the server. I'd like it to go ahead and create the So if I'm a user of the Adding a For actions and bind:this, bind:width, etc, the compiler could automatically add |
@wickning1 Yes! That's what I'm saying! I believe the Svelte tutorial even suggests patterns like always adding a "klass" field exported as "class" to support adding extra classes to a component, which seems like a very frequent symptom of a more general problem. Some components "are" a single element semantically--it's just been enhance and been given children. The fact that Svelte supports multiple root elements for components and you explicitly see all the root elements in the markup to avoid things like weird ":host" CSS selectors (such as in Angular) and you can get away with more powerful DOM is critical imo, but they are missing out on all the cases where Angular's design decisions actually worked out. I feel like we could easily have the best of both worlds. |
This is just a philosophy thing for me but, I think good Components should be indistinguishable from the default browser components. Frameworks, just give us the ability to extend the default components with our own. I agree with the sentiment in this issue. The fact that Svelte components have inherently different rules than default html components makes Svelte hard to scale. Since right now basically we have Svelte Components for everything and it makes actions completely unusable for us. I get that there is the workaround with the action prop but, I think that introduces a lot of frustrating cognitive load to the language since you need to now learn how to use actions on Svelte Components in a hacky non documented way. Whereas, if they just behaved consistently across the board it would be way easier to work with. Also with this it would be nice if we could also use it for the |
This is a pattern that I've become really reliant on in React and I was surprised to see there's no clear analogue in Svelte: type MyButtonProps = React.ComponentProps<'button'> & {
theme: 'primary' | 'secondary';
size: 'sm' | 'md' | 'lg';
}
export default function MyButton(props: MyButtonProps) {
const { theme, size, children, ...buttonProps } = props;
// Not pictured: using theme and size to create classNames for the button
return <button {...buttonProps}>{children}</button>;
} This gives the user a type-safe way to apply any event or attribute to the underlying button they want, and basically work with the |
If this is added, we could use style-directives on components. <!-- App.svelte -->
<Card style:padding=".5em">
Hello World!
</Card> <!-- Card.svelte -->
<div svelte:host>
<slot />
</div> |
Yes, and any of the other directives you can apply to normal elements, including actions. That does make me wonder if the feature would cause optimization headaches because Svelte might currently make a lot of assumptions about knowing all directives applied to an element just from the template it lives in. Whereas now it would need to expose API for parent components to dynamically add in more style/class/action/event/etc. directives. I do not know anything about Svelte internals and that may have been a reason the Svelte core contributors largely brushed off this feature request when I first made it. |
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.
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, theMatButton
component must support atooltip
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:Because the
MatButton
component gets replaced by a singlebutton
element, I think it is very intuitive for users of the library to think of theMatButton
and thebutton
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
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.For my simple tooltip example, I could create a
TooltipHitbox
component with a<slot/>
inside a<div use:myTooltip={tooltipProp}>
and then wrapMatButton
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.
The text was updated successfully, but these errors were encountered: