svelte icon indicating copy to clipboard operation
svelte copied to clipboard

Dynamic slot names

Open csangonzo opened this issue 3 years ago • 22 comments

Describe the problem

I'm building an extensible data table component, it works pretty well now, but the only thing missing is slots with dynamic names. So the way the table works is you can set column definitions and the data, the column definitions contain the key that matches the key in the data e.g. key: "name" and if there's a name: "something" in the data, the name column has values. Right now I have a button, icon and other action types available so when the dev. sets a column to a button type it shows a button, so far so good. The only problem with this is that only my predefined button styles are available and what would be a great solution is to define the column type as custom and when I set the key to testColumn it would look for a slot named testColumn, so in the table component:

{#if col.type === "custom"}
<!-- col.key will be "testColumn" -->
	<slot name={col.key}>
	   Custom fallback
	</slot>
{/if}

and I'd pass the matching slot outside of the component:

<DataTable bind:cols bind:data>
<!-- Could contain a button, icon div or anything we'd like -->
    <div slot="testColumn">
        This is a custom column.
    </div>
</DataTable>

( There was an issue regarding this, but it was submitted as a bug rather than a feature request and lacked detailed explanation: https://github.com/sveltejs/svelte/issues/5859 )

Describe the proposed solution

It would be really awesome if we could do something like this in svelte ( if I'm correct vue has something like this: https://vuejs.org/v2/guide/components-slots.html#Dynamic-Slot-Names ), it would open up whole new possibilities for svelte components.

Alternatives considered

It doesn't necessary have to work with slots like I mentioned, I'm open to any 'svelter' solution as well.

Importance

would make my life easier

csangonzo avatar Jul 05 '21 06:07 csangonzo

I made it work like this for now:

...
{:else if col.type === 'custom'}
    <slot col={col} value={row.data[col.key]}></slot>
{/if}
...

And inside the component:

<DataTable bind:cols bind:data
let:col
let:value>
<!-- Could contain a button, icon div or anything we'd like -->
      {#if col.key === 'custom1'}
        <span>
          <button
          on:click={(e) => {
            e.stopPropagation();
            console.log('asdasd')
          }}
          >TEST</button>
        </span>
      {:else if col.key === 'custom2'}
        {#each value as v}
          <p>{v}</p>
        {/each}
      {:else if col.key === 'bool'}
        {#if value}
          TRUE - {value} - {col.title}
        {:else}
          FALSE - {value}
        {/if}
      {/if}
</DataTable>

https://svelte.dev/docs#slot_let

csangonzo avatar Jul 05 '21 09:07 csangonzo

I have this problem too, I build custom DataTable too 🤣

Btw thanks @csangonzo for the workaround, I like your idea. But it's still hard to use. Still far from ideal dynamic named slot. Hope this feature coming to svelte soon. This framework is awesome and very fast

axmad386 avatar Aug 11 '21 04:08 axmad386

I have this problem too, I build custom DataTable too 🤣

vnaki avatar Aug 17 '21 13:08 vnaki

this problem is pretty common , if you build tab ui

pradeep-mishra avatar Sep 12 '21 11:09 pradeep-mishra

I wonder if there is a common way to deal with this case. Is this the reason at compile time? 😭

robin-shine avatar Oct 13 '21 09:10 robin-shine

an another method maybe helps you https://github.com/yus-ham/blangko/blob/db7e475c704454d418e1f6e7331452932e6a5b6d/src_front/pages/posyandu/index.svelte#L13-L38

yus-ham avatar Feb 24 '22 06:02 yus-ham

In addition to the datatable example, I'd like to add another use case regarding advanced i18n interpolation.

For example, if we want to create something like this User clicked <FancyLookingNumber>{2}</FancyLookingNumber> times. It would be great if we can do this in svelte:

<Trans message={t("user-click")}>
  <FancyLookingNumber slot="count">{2}</FancyLookingNumber>
</Trans>

In English, t("user-click") returns User clicked {count} times. The Trans component extracts count from the text and create a count named slot dynamically.

crimx avatar Apr 08 '22 05:04 crimx

In addition to the datatable example, I'd like to add another use case regarding advanced i18n interpolation.

For example, if we want to create something like this User clicked <FancyLookingNumber>{2}</FancyLookingNumber> times. It would be great if we can do this in svelte:

<Trans message={t("user-click")}>
  <FancyLookingNumber slot="count">{2}</FancyLookingNumber>
</Trans>

In English, t("user-click") returns User clicked {count} times. The Trans component extracts count from the text and create a count named slot dynamically.

My workaround involves passing an array prop that gives the names of the slots, pregenerating some named slots with numbers, binding the contents to a variable, and then just setting the children of the element once my translation is looked up.

Here's what it looks like: LocalizedText.svelte

<script>
    import { onMount, onDestroy } from "svelte";
    import App from "../app.mjs";
    import translate from "../translate.mjs";

    const unregister = [];

    export let opts = {};
    export let key;

    export let slotMapping = [];
    let slotContents = {};

    let language;
    let contentElement;

    function render() {
        if (language) {
            const { result, usedFallback } = translate(
                language,
                key,
                opts,
                false
            );

            let newContents;

            if (usedFallback) {
                newContents = `<span style="background: red" title="NO TRANSLATION KEY">${result}</span>`;
            } else {
                newContents = result;
            }

            newContents = newContents.split(/(%\w+%)/g);

            for (const [index, value] of newContents.entries()) {
                if (value.startsWith("%")) {
                    const slotName = value.slice(1, -1);
                    const slotId = "item" + slotMapping.indexOf(slotName);

                    newContents[index] = slotContents[slotId];
                } else {
                    const span = document.createElement("span");
                    span.innerHTML = value;
                    newContents[index] = span;
                }
            }

            if (slotMapping.length > 0) {
                console.debug(
                    "[LocalizedText] localized with slots:",
                    key,
                    slotMapping,
                    result,
                    newContents
                );
            }

            contentElement?.replaceChildren(...newContents);
        } else {
            contentElement?.replaceChildren(...[key]);
        }
    }

    onMount(() => {
        unregister.push(
            App.on("language", (l) => {
                language = l;
                render();
            })
        );
    });

    onDestroy(() => {
        for (const un of unregister) {
            try {
                App.off(un[0], un[1]);
            } catch (ignored) {}
        }
    });

    // Rerender on change
    $: key, render();
    $: opts, render();
</script>

<div style="display: none;">
    <!-- We need 10 of these -->
    <span bind:this={slotContents.item0}><slot name="0" /></span>
    <span bind:this={slotContents.item1}><slot name="1" /></span>
    <span bind:this={slotContents.item2}><slot name="2" /></span>
    <span bind:this={slotContents.item3}><slot name="3" /></span>
    <span bind:this={slotContents.item4}><slot name="4" /></span>
    <span bind:this={slotContents.item5}><slot name="5" /></span>
    <span bind:this={slotContents.item6}><slot name="6" /></span>
    <span bind:this={slotContents.item7}><slot name="7" /></span>
    <span bind:this={slotContents.item8}><slot name="8" /></span>
    <span bind:this={slotContents.item9}><slot name="9" /></span>
</div>

<span bind:this={contentElement} />

Example usage (abridged):

                    <LocalizedText key="chatbot_manager.commands.format.{command.type}" slotMapping={["platform", "action", "action_target", "message"]}>
                        <div class="select" slot="0">
                            <select bind:value={command.platform}>
                                {#each PLATFORMS as platform}
                                    <option value={platform[0]}>{platform[1]}</option>
                                {/each}
                            </select>
                        </div>

                        <div class="select" slot="1">
                            <select bind:value={command.type}>
                                <option value="COMMAND">
                                    <LocalizedText key="chatbot_manager.commands.runs" />
                                </option>
                                <option value="CONTAINS">
                                    <LocalizedText key="chatbot_manager.commands.mentions" />
                                </option>
                            </select>
                        </div>

                        <span slot="2">
                            <input class="input" type="text" bind:value={command.trigger} style="width: 200px" />
                        </span>

                        <span slot="3">
                            <br />
                            <textarea class="textarea" bind:value={command.response} rows={2} />
                        </span>
                    </LocalizedText>

My lang file: ({...} is a passed in prop, [...] is a lang key lookup prop, and %...% is an element prop)

{
    "chatbot_manager.commands.format.COMMAND": "When someone from %platform% %action% !%action_target%, send %message%",
    "chatbot_manager.commands.format.CONTAINS": "When someone from %platform% %action% [generic.leftquote]%action_target%[generic.rightquote], reply with %message%"
}

Obviously, this is pretty dirty but it works. My ideal would look like:

<script>
    // ... lookup translation, yada yada
</script>

{#if contents}
    {#each contents as item}
        {#if item.startsWith('%')}
            <slot name={item.slice(1, -1) /* trim off the %'s */} />
        {:else}
            {@html item}
        {/if}
    {/each}
{:else}
    {key} <!-- Fallback -->
{/if}

e3ndr avatar Apr 27 '22 16:04 e3ndr

Would love to see this feature as well!

winston0410 avatar Jul 03 '22 09:07 winston0410

Yes, please. We absolutely need this!

pejeio avatar Jul 08 '22 21:07 pejeio

@Conduitry Is there a fix for the dynamic slot name problem in the svelte developers plans?

amirhossein-fzl avatar Jul 17 '22 14:07 amirhossein-fzl

Is there any update on this feature?

cdebadri avatar Jul 30 '22 15:07 cdebadri

In addition to the datatable example, I'd like to add another use case regarding advanced i18n interpolation.

For example, if we want to create something like this User clicked <FancyLookingNumber>{2}</FancyLookingNumber> times. It would be great if we can do this in svelte:

<Trans message={t("user-click")}>
  <FancyLookingNumber slot="count">{2}</FancyLookingNumber>
</Trans>

In English, t("user-click") returns User clicked {count} times. The Trans component extracts count from the text and create a count named slot dynamically.

Just coming here to add my voice to this, I was halfway through recreating this exact solution for my own translation project and ran into this limitation. I'll likely go with @e3nder's suggestion of having a number of "dummy" slots but it's a bit disappointing that this isn't possible!

nornagon avatar Aug 07 '22 20:08 nornagon

My another solution is not using slot, but using <svelte:component/> The ideas is just pass the custom component (in my case is custom column, because I build DataTable) into props. And inside the DataTable just check if there is custom component, render it using <svelte:component/>. Is not ideal, by it's dynamic and fit with my project. This is the simplified version https://svelte.dev/repl/10d9d08f95d1496eb751b81f9e3271b3?version=3.49.0

axmad386 avatar Aug 08 '22 02:08 axmad386

My another solution is not using slot, but using <svelte:component/> The ideas is just pass the custom component (in my case is custom column, because I build DataTable) into props. And inside the DataTable just check if there is custom component, render it using <svelte:component/>. Is not ideal, by it's dynamic and fit with my project. This is the simplified version https://svelte.dev/repl/10d9d08f95d1496eb751b81f9e3271b3?version=3.49.0

Good solution as well, though I personally prefer my data and text to be in the markup and not in the JS. Still, any solution is better than none :^)

e3ndr avatar Aug 08 '22 02:08 e3ndr

For those who are building custom datatable like what I did, my solution is as below:

<!-- Datatable.svelte -->
{#each data as row}
    ...
    {#each columns as column}
        <td>
            {#if column.transform}
                {column.transform(row)}
            {:else}
                {row[column.id]}
            {/if}
        </td>
    {/each}
    ...
{/each}}

<!-- Parent -->
<script>
    import Datatable from '@components/Datatable/Datatable.svelte';

    const columns = [
        {
            id: 'column_1',
            label: 'Column 1',
            transform: (row) => row.tableRelation.targetColumn,
        },
        {
            id: 'column_2',
            label: 'Column 2',
        }
    ];

    const data = [
        {
            id: 1,
            foo: bar,
            column_2: 'Column 2 Row Data'
            tableRelation: [
                {
                    targetColumn: "Table Relation Row 1",
                    foo: bar
                }
            ]
        },
        {
            id: 1,
            foo: bar,
            column_2: 'Column 2 Row Data'
            tableRelation: [
                {
                    targetColumn: "Table Relation Row 2",
                    foo: bar
                }
            ]
        }
    ]
</script>

<Datatable {columns} {data} />

This works in my situation, hope it will be helpful.

wilson-shen avatar Dec 25 '22 10:12 wilson-shen

Any updates on the issue?

AngelCode-dev avatar Mar 22 '23 19:03 AngelCode-dev

Any updates on the issue?

I do not know

amirhossein-fzl avatar Mar 23 '23 15:03 amirhossein-fzl

Is there an update when will this be released?

yourcharlie23 avatar Jul 24 '23 07:07 yourcharlie23

https://github.com/sveltejs/svelte/pull/8535 this pull request seems successful, why don't they evaluate it? @Rich-Harris @Conduitry @tanhauhau @dummdidumm

ByEBA avatar Aug 07 '23 00:08 ByEBA

This is the main issue with svelte I'm having, and it the absence of this feature keeps hindering my development. I want to strongly voice my support for this!

AbstractFruitFactory avatar Jan 25 '24 09:01 AbstractFruitFactory