huntabyte / shadcn-svelte

shadcn/ui, but for Svelte. ✨
https://shadcn-svelte.com
MIT License
4.38k stars 276 forks source link

Export `action` in components for custom events #422

Open justind000 opened 7 months ago

justind000 commented 7 months ago

Describe the feature in detail (code, mocks, or screenshots encouraged)

I was adding in addResizedColumns to the Data Table and needed to pass use:props.resize to the Table.Head component. <Table.Head {...attrs} use:props.resize>. This gives an error, "Actions can only be applied to DOM elements, not componentssvelte(invalid-action)".

To fix it, I modified table-head.svelte to this:

<script lang="ts">
    import { cn } from "$lib/utils";
    import type { HTMLThAttributes } from "svelte/elements";

    type $$Props = HTMLThAttributes;

    let className: $$Props["class"] = undefined;
    export let action = () => {};
    export let actionParams = undefined;
    export { className as class };
</script>

<th use:action={actionParams}
    class={cn(
        "h-12 px-4 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
        className
    )}
    {...$$restProps}
>
    <slot />
</th>

Now you can pass props.resize like so: <Table.Head {...attrs} action={props.resize}>

SMUI and I'm assuming others, do something similar to this for all the components.

What type of pull request would this be?

New Feature

Provide relevant links or additional information.

No response

huntabyte commented 7 months ago

I think this is definitely something we can/should do!

huntabyte commented 7 months ago

My initial thoughts are we want this to be type-safe and flexible. I think taking inspiration from SMUI is a good idea to see how they handle it. It appears as though they accept an array of actions, but I need to investigate further how they go about handling/typing the params in that way.

We could also accept a single action, since actions are just functions, you can apply multiple "actions" within a single action, for example:

import externalAction from 'wherever'

function myCustomAction(node) {
    const actReturn = externalAction(node)
    return {
        destroy() {
            if (actReturn?.destroy) {
                actReturn.destroy()
            }
        }
    }   
}
Carlos-err406 commented 4 months ago

i would like to contribute on this if thats ok @huntabyte

edit: i also think is worth looking at the smui approach

huntabyte commented 4 months ago

How do you plan to implement/type this and how will updates to the actions be handled?

Carlos-err406 commented 4 months ago

How do you plan to implement/type this and how will updates to the actions be handled?

i was planning on doing some proof of concept first regarding the types, im aware that typesafety is a must

regarding the update i was going to ask for guidance on the discord channel but links are expired but my idea was on updating component per component

Carlos-err406 commented 4 months ago

@huntabyte i was just checking on this issue and of course i tried first to add an action to the Button component

this is the shadcn-svelte Button

<script lang="ts">
    import { Button as ButtonPrimitive } from "bits-ui";
    import { cn } from "$lib/utils";
    import { buttonVariants, type Props, type Events } from ".";

    type $$Props = Props;
    type $$Events = Events;

    let className: $$Props["class"] = undefined;
    export let variant: $$Props["variant"] = "default";
    export let size: $$Props["size"] = "default";
    export let builders: $$Props["builders"] = []; //<----------
    export { className as class };
</script>

<ButtonPrimitive.Root
    {builders} //<------------
    class={cn(buttonVariants({ variant, size, className }))}
    type="button"
    {...$$restProps}
    on:click
    on:keydown
>
    <slot />
</ButtonPrimitive.Root>

and this builder prop catched my eye so i digged into it a little the type Builder is an object with an action attribute and this action is of type { Action } from "svelte/action"

in this case the shadcn-svelte Button is taking a Builder array so its possible to pass actions to the component... just not by using use:

<script lang="ts">
    import { Button } from "$lib/components/ui/button";
    const myAction = (node: HTMLElement) => {
        console.log({ node });
    };
</script>

<Button builders={[{ action: myAction }]}>Hello world</Button>

or pass extra arguments

<script lang="ts">
    import { Button } from "$lib/components/ui/button";
    interface ExtraArgs {
        a?: string;
        b?: boolean;
    }
    const myAction = (node: HTMLElement, { a = "", b }: ExtraArgs) => {
        console.log({ node, a, b });
    };
</script>

<Button builders={[{ action: (node) => myAction(node, { a: "This is an extra", b: true }) }]}>
    Hello world
</Button>

though i think there could be an implementation to use the use directive or at least as a prop, this works for the meantime

Carlos-err406 commented 4 months ago

some components like <Card> (and card slots) dont use bits-ui, are just styled divs and maybe adding it to those components is a start?