I am developing a DataTable
component in Svelte with this usage:
<script>
import DataTable from './DataTable.svelte'
import Column from './Column.svelte'
let items = [
{id: 1, name: 'Name 1', age: 23},
{id: 2, name: 'Name 2', age: 28},
{id: 3, name: 'Name 3', age: 35},
{id: 4, name: 'Name 4', age: 18},
]
</script>
<DataTable {items} let:item>
<Column title="Id">{item.id}</Column>
<Column title="Customer">{item.name}</Column>
<Column title="Age">{item.age}</Column>
</DataTable>
The Column
component just works as a meta definition for DataTable
component. There are some other ways to define columns
in script
section (like this), but in order to be more focused on business logic in script
section, I'm trying to reduce component's visual definitions/configurations in the script
section.
Therefore, the default slot
content in Column
component should be passed to parent DataTable
component and then rendered in the cells. In other words, parent component should take control of child component's slots.
DataTable.svelte:
<script>
import { setContext, getContext, onDestroy } from 'svelte';
export let items = []
let columns = []
setContext('columns', {
register: column => {
columns.push(column)
onDestroy(() => {
const i = columns.indexOf(column)
columns.splice(i, 1)
})
},
getColumns: () => {return columns}
})
</script>
<slot />
{#if items}
<table width="100%" border="1">
<thead>
<tr>
{#each columns as column}
<th>
{column.title}
</th>
{/each}
</tr>
</thead>
{#each items as item1, i}
<tr>
{#each columns as column}
<td>
{JSON.stringify(column.$$scope.ctx[0][i])}
</td>
{/each}
</tr>
{/each}
</table>
{/if}
Column.svelte:
<script>
import { getContext } from 'svelte';
let columnProps = {...$$props}
const { register } = getContext('columns')
register(columnProps)
</script>
In this line: {JSON.stringify(column.$$scope.ctx[0][i])}
the bound row is shown correctly (not sure if it works in all cases because index 0 is hardcoded!).
But, the content should be rendered using the first, second, etc. column's slots ({item.id}
,{item.name}
, etc.)
How can I render slot dynamically or in the parent component to show the correct slot content?
Also, as slot value passed to Column
in the above usage (without any slot inside the child), Svelte raises warning (<Column> received an unexpected slot "default".)
which is completely correct!
What is the correct way of implementing this DataTable
component?
The correct way of doing this is probably to not do it like this at all. I worked on implementing a tab control in a similar fashion and Svelte seems to be not so great at interacting with slots/child components that are used in multiple places.
To make this work to some degree you can apply a few changes:
Column
:<td>
<slot />
</td>
DataTable
(it already contains all columns):{#each items as item, i}
<tr>
<slot {item} />
</tr>
{/each}
Remove that rogue <slot/>
in DataTable
after the <script>
tag which will break since it has no item
.
Reassign columns
, since it won't update otherwise; also check whether the column has been registered yet, otherwise you get duplicates (one for each row):
register: column => {
if (columns.find(c => c.title == column.title))
return;
columns = [...columns, column];
onDestroy(() => {
columns = columns.filter(c => c.title != column.title);
})
},
You will probably regret this once you figure that the title
should be arbitrary DOM content and not just a string.
You should definitely not access something like $$scope.ctx
, that is an implementation detail that can change at any point.