svelte
svelte copied to clipboard
Dynamic slot names
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
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
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
I have this problem too, I build custom DataTable too 🤣
this problem is pretty common , if you build tab ui
I wonder if there is a common way to deal with this case. Is this the reason at compile time? 😭
an another method maybe helps you https://github.com/yus-ham/blangko/blob/db7e475c704454d418e1f6e7331452932e6a5b6d/src_front/pages/posyandu/index.svelte#L13-L38
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.
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")
returnsUser clicked {count} times
. TheTrans
component extractscount
from the text and create acount
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}
Would love to see this feature as well!
Yes, please. We absolutely need this!
@Conduitry Is there a fix for the dynamic slot name problem in the svelte developers plans?
Is there any update on this feature?
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")
returnsUser clicked {count} times
. TheTrans
component extractscount
from the text and create acount
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!
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
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 :^)
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.
Any updates on the issue?
Any updates on the issue?
I do not know
Is there an update when will this be released?
https://github.com/sveltejs/svelte/pull/8535 this pull request seems successful, why don't they evaluate it? @Rich-Harris @Conduitry @tanhauhau @dummdidumm
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!