Skip to content

Dynamic slot names #6493

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

Closed
csangonzo opened this issue Jul 5, 2021 · 22 comments
Closed

Dynamic slot names #6493

csangonzo opened this issue Jul 5, 2021 · 22 comments
Labels
compiler Changes relating to the compiler feature request
Milestone

Comments

@csangonzo
Copy link

csangonzo commented Jul 5, 2021

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: #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

@dummdidumm dummdidumm added compiler Changes relating to the compiler feature request labels Jul 5, 2021
@csangonzo
Copy link
Author

csangonzo commented Jul 5, 2021

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

@axmad386
Copy link

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

@vnaki
Copy link

vnaki commented Aug 17, 2021

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

@pradeep-mishra
Copy link

pradeep-mishra commented Sep 12, 2021

this problem is pretty common , if you build tab ui

@robin-shine
Copy link

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

@yus-ham
Copy link
Contributor

yus-ham commented Feb 24, 2022

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

@crimx
Copy link

crimx commented Apr 8, 2022

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.

@e3ndr
Copy link

e3ndr commented Apr 27, 2022

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}

@winston0410
Copy link

Would love to see this feature as well!

@pejeio
Copy link

pejeio commented Jul 8, 2022

Yes, please. We absolutely need this!

@amirhossein-fzl
Copy link

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

@cdebadri
Copy link

Is there any update on this feature?

@nornagon
Copy link

nornagon commented Aug 7, 2022

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!

@axmad386
Copy link

axmad386 commented Aug 8, 2022

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

@e3ndr
Copy link

e3ndr commented Aug 8, 2022

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 :^)

@wilson-shen
Copy link

wilson-shen commented Dec 25, 2022

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.

@aypm-dev
Copy link

Any updates on the issue?

@amirhossein-fzl
Copy link

Any updates on the issue?

I do not know

@yourcharlie23
Copy link

Is there an update when will this be released?

@ByEBA
Copy link

ByEBA commented Aug 7, 2023

#8535
this pull request seems successful, why don't they evaluate it?
@Rich-Harris @Conduitry @tanhauhau @dummdidumm

@AbstractFruitFactory
Copy link

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!

@dummdidumm
Copy link
Member

dummdidumm commented Jan 25, 2024

This will be possible using snippets in Svelte 5

@dummdidumm dummdidumm added this to the 5.0 milestone Jan 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler Changes relating to the compiler feature request
Projects
None yet
Development

No branches or pull requests