Moving templates to current.*
Since v6.4, every row, cell and the view model itself expose a current object whose values are plain and reactive. A template that reads them needs no <Subscribe> wrapper, no fromStore, and no $ prefix. This guide converts an existing store-based template to current.*.
$tableAttrs, cell.attrs(), cell.props(), <Subscribe>) keeps working unchanged, and both views can be mixed in one template. Convert when it makes a template simpler.Hand this to your AI assistant
This conversion is mechanical, which makes it a good job for a coding assistant. Every page of these docs is also published as plain Markdown. Copy this prompt into your assistant from your project’s root:
Convert this project's @humanspeak/svelte-headless-table templates from the
store API to current.*, staying on version 6.
1. Read https://table.svelte.page/docs/guides/moving-to-current.md in full
before changing anything.
2. Keep the package on 6.x: the installed version must satisfy ^6.4.0. Do not
upgrade to 7 in this change.
3. Apply the guide's "Step by step" section to every template: keep the view
model in one variable, replace $tableAttrs / $headerRows / $rows /
$pageRows with vm.current.*, unwrap every <Subscribe> and read
x.current.attrs and x.current.props, and move actions to
use:cell.current.props.<plugin>.
4. Change templates only. Leave column definitions, createRender calls,
plugins and plugin state ($sortKeys, pageIndex.set(...)) exactly as they
are: on 6.x they are still stores.
5. Work through the guide's "Checklist", then remove the Subscribe import
where it is no longer used. Run the project's type check and tests and
report which files you changed.Convert this project's @humanspeak/svelte-headless-table templates from the
store API to current.*, staying on version 6.
1. Read https://table.svelte.page/docs/guides/moving-to-current.md in full
before changing anything.
2. Keep the package on 6.x: the installed version must satisfy ^6.4.0. Do not
upgrade to 7 in this change.
3. Apply the guide's "Step by step" section to every template: keep the view
model in one variable, replace $tableAttrs / $headerRows / $rows /
$pageRows with vm.current.*, unwrap every <Subscribe> and read
x.current.attrs and x.current.props, and move actions to
use:cell.current.props.<plugin>.
4. Change templates only. Leave column definitions, createRender calls,
plugins and plugin state ($sortKeys, pageIndex.set(...)) exactly as they
are: on 6.x they are still stores.
5. Work through the guide's "Checklist", then remove the Subscribe import
where it is no longer used. Run the project's type check and tests and
report which files you changed.If your assistant cannot fetch URLs, paste the contents of moving-to-current.md after the prompt. When this is done and shipped, Migrating to v7 has the prompt for the next step.
What changes and what does not
| Before | After |
|---|---|
const { headerRows, rows, tableAttrs, … } = table.createViewModel(columns) | const vm = table.createViewModel(columns) |
{...$tableAttrs} / {...$tableBodyAttrs} | {...vm.current.tableAttrs} / {...vm.current.tableBodyAttrs} |
{#each $headerRows as headerRow} | {#each vm.current.headerRows as headerRow} |
{#each $rows as row} / {#each $pageRows as row} | {#each vm.current.rows as row} / {#each vm.current.pageRows as row} |
<Subscribe attrs={cell.attrs()} let:attrs> … {...attrs} | {...cell.current.attrs} |
<Subscribe props={cell.props()} let:props> … props.sort.order | cell.current.props.sort.order |
<Subscribe rowAttrs={row.attrs()} let:rowAttrs> … {...rowAttrs} | {...row.current.attrs} |
use:props.resize | use:cell.current.props.resize |
<Render of={cell.render()} /> | unchanged |
pluginStates.sort.sortKeys and every other plugin store | unchanged on 6.x (store reads still work); on v7 read sortKeys.current |
Column definitions, createRender, snippets, plugins | unchanged on 6.x; on v7 createRender takes a props getter instead of a store |
current.props has exactly the shape props() had: one key per plugin name, with that plugin’s props inside. Only the way you read it changes.
Step by step
1. Keep the view model in one variable
Destructuring the stores is what forced $-prefixed reads at the top level of the script. Keep vm instead and pull out only what you still need as stores (usually plugin state):
<script>
// before
const { headerRows, pageRows, tableAttrs, tableBodyAttrs, pluginStates } =
table.createViewModel(columns)
const { sortKeys } = pluginStates.sort
// after
const vm = table.createViewModel(columns)
const { sortKeys } = vm.pluginStates.sort
</script><script>
// before
const { headerRows, pageRows, tableAttrs, tableBodyAttrs, pluginStates } =
table.createViewModel(columns)
const { sortKeys } = pluginStates.sort
// after
const vm = table.createViewModel(columns)
const { sortKeys } = vm.pluginStates.sort
</script>2. Replace the table-level stores
<!-- before -->
<table {...$tableAttrs}>
<tbody {...$tableBodyAttrs}>
{#each $pageRows as row (row.id)}
<!-- after -->
<table {...vm.current.tableAttrs}>
<tbody {...vm.current.tableBodyAttrs}>
{#each vm.current.pageRows as row (row.id)}<!-- before -->
<table {...$tableAttrs}>
<tbody {...$tableBodyAttrs}>
{#each $pageRows as row (row.id)}
<!-- after -->
<table {...vm.current.tableAttrs}>
<tbody {...vm.current.tableBodyAttrs}>
{#each vm.current.pageRows as row (row.id)}Use vm.current.rows where you used $rows and vm.current.pageRows where you used $pageRows; the pagination and virtual-scroll plugins act on pageRows exactly as before.
3. Unwrap <Subscribe> and read current directly
<!-- before -->
<Subscribe rowAttrs={headerRow.attrs()} let:rowAttrs>
<tr {...rowAttrs}>
{#each headerRow.cells as cell (cell.id)}
<Subscribe attrs={cell.attrs()} let:attrs props={cell.props()} let:props>
<th {...attrs} onclick={props.sort.toggle} class:sorted={props.sort.order}>
<Render of={cell.render()} />
</th>
</Subscribe>
{/each}
</tr>
</Subscribe>
<!-- after -->
<tr {...headerRow.current.attrs}>
{#each headerRow.cells as cell (cell.id)}
<th
{...cell.current.attrs}
onclick={cell.current.props.sort.toggle}
class:sorted={cell.current.props.sort.order}
>
<Render of={cell.render()} />
</th>
{/each}
</tr><!-- before -->
<Subscribe rowAttrs={headerRow.attrs()} let:rowAttrs>
<tr {...rowAttrs}>
{#each headerRow.cells as cell (cell.id)}
<Subscribe attrs={cell.attrs()} let:attrs props={cell.props()} let:props>
<th {...attrs} onclick={props.sort.toggle} class:sorted={props.sort.order}>
<Render of={cell.render()} />
</th>
</Subscribe>
{/each}
</tr>
</Subscribe>
<!-- after -->
<tr {...headerRow.current.attrs}>
{#each headerRow.cells as cell (cell.id)}
<th
{...cell.current.attrs}
onclick={cell.current.props.sort.toggle}
class:sorted={cell.current.props.sort.order}
>
<Render of={cell.render()} />
</th>
{/each}
</tr>Every let: name maps to the matching current property on the same object: attrs → x.current.attrs, props → x.current.props. Body rows are identical with row in place of headerRow and <td> in place of <th>.
4. Actions, class directives and conditionals
They all read through current.props the same way:
<th {...cell.current.attrs} use:cell.current.props.resize>
{#if cell.current.props.filter?.render}
<Render of={cell.current.props.filter.render} />
{/if}
{#if !cell.current.props.resize.disabled}
<div class="resizer" use:cell.current.props.resize.drag></div>
{/if}
</th>
<tr {...row.current.attrs} class:selected={row.current.props.select.selected}><th {...cell.current.attrs} use:cell.current.props.resize>
{#if cell.current.props.filter?.render}
<Render of={cell.current.props.filter.render} />
{/if}
{#if !cell.current.props.resize.disabled}
<div class="resizer" use:cell.current.props.resize.drag></div>
{/if}
</th>
<tr {...row.current.attrs} class:selected={row.current.props.select.selected}>5. Delete the import
Remove Subscribe from the import list once nothing in the file uses it. Render stays.
Reading current outside the template
current values are also correct outside a template or effect; they simply do not track. That makes them safe in event handlers and plain functions:
const selectedNames = () =>
vm.current.rows.filter((row) => row.current.props.select.selected).map((row) => row.original.name)const selectedNames = () =>
vm.current.rows.filter((row) => row.current.props.select.selected).map((row) => row.original.name)Inside a $derived or $effect, the same reads track and re-run as state changes.
Things to know
- Plugin state is still stores on 6.x.
pluginStates.<plugin>(sortKeys,pageIndex,selectedDataIds,filterValue, …) is unchanged, so$-prefixed reads andpageIndex.set(…)keep working. Only the view-model and component read side gainedcurrent. On v7 plugin state moves tocurrenttoo (pageIndex.current); see Migrating to v7. - Server rendering produces the same markup through
currentas through the stores. No change is needed for SvelteKit SSR. - Mixing is fine. A template can read
vm.current.pageRowsand still wrap one cell in<Subscribe>, or read$pageRowsand userow.current.attrs. The two views are backed by the same derivation chain. fromStoreis no longer needed for table stores. If you adopted{@const attrs = fromStore(cell.attrs())}in the meantime, replace it withcell.current.attrs.- Custom cell components created with
createRender(Component, props)receive whatever you pass; if you passed a store, keep passing it.currentdoes not change how cell content is rendered.
Checklist
const vm = table.createViewModel(columns); pull plugin stores fromvm.pluginStates.$tableAttrs→vm.current.tableAttrs; same fortableHeadAttrsandtableBodyAttrs.$headerRows/$rows/$pageRows→vm.current.headerRows/rows/pageRows.- Remove each
<Subscribe … let:…>; readx.current.attrsandx.current.props. - Actions:
use:cell.current.props.<plugin>…. - Drop the
Subscribeimport. LeaveRender, plugin state and column definitions alone.
See TableViewModel, BodyCell and the quick start for the resulting template in full.