Skip to content

Editing (CRUD)

Create, edit and delete users. Edits go through a dialog, validated in the save handler: returning without calling exitEditingMode() keeps it open. In a real app, replace the local data updates with requests. See the Editing guide.

The Create button is the shadcn-svelte Button, which the package re-exports.

EditingCrud.svelte
<script lang="ts">
import { Button } from 'shadcn-svelte-tanstack-table';
import {
MRT_ActionMenuItem,
ShadcnSvelteTable,
createShadcnSvelteTable,
type MRT_ColumnDef,
type MRT_Row,
type MRT_TableInstance,
} from 'shadcn-svelte-tanstack-table';
import { type Person, makeData, states } from '../data/makeData';
let data = $state(makeData(12));
let validationErrors = $state<Record<string, string>>({});
const columns: MRT_ColumnDef<Person>[] = [
// the first 8 characters of the UUID keep the column narrow
{ accessorFn: (row) => row.id.slice(0, 8), enableEditing: false, header: 'Id', id: 'id', size: 110 },
{ accessorKey: 'firstName', header: 'First Name' },
{ accessorKey: 'lastName', header: 'Last Name' },
{ accessorKey: 'email', header: 'Email' },
{
accessorKey: 'state',
editSelectOptions: states,
editVariant: 'select',
header: 'State',
},
];
/** Returns an error per invalid field. */
const validate = (values: Record<string, unknown>) => {
const errors: Record<string, string> = {};
if (!values.firstName) errors.firstName = 'First name is required';
if (!values.lastName) errors.lastName = 'Last name is required';
if (!/^\S+@\S+\.\S+$/.test(String(values.email ?? ''))) errors.email = 'Enter a valid email';
return errors;
};
// The table instance is needed to start creating from the toolbar button.
const table = createShadcnSvelteTable<Person>({
columns,
createDisplayMode: 'modal',
get data() {
return data;
},
editDisplayMode: 'modal',
enableEditing: true,
enableRowActions: true,
getRowId: (row) => row.id,
onCreatingRowCancel: () => (validationErrors = {}),
onCreatingRowSave: ({ exitCreatingMode, values }) => {
validationErrors = validate(values);
if (Object.keys(validationErrors).length) return; // keep the dialog open
data = [{ ...(values as unknown as Person), id: crypto.randomUUID() }, ...data];
exitCreatingMode();
},
onEditingRowCancel: () => (validationErrors = {}),
onEditingRowSave: ({ exitEditingMode, row, values }) => {
validationErrors = validate(values);
if (Object.keys(validationErrors).length) return;
data = data.map((person) => (person.id === row.id ? { ...person, ...values } : person));
exitEditingMode();
},
// show the errors on the fields
editTextFieldProps: ({ cell }) => ({
input$onfocus: () => (validationErrors = { ...validationErrors, [cell.column.id]: '' }),
invalid: !!validationErrors[cell.column.id],
}),
rowActionMenuItems: rowActions,
topToolbarCustomActions: createButton,
});
</script>
{#snippet rowActions(ctx: {
closeMenu: () => void;
row: MRT_Row<Person>;
table: MRT_TableInstance<Person>;
})}
<MRT_ActionMenuItem
label="Delete"
onclick={() => {
data = data.filter((person) => person.id !== ctx.row.id);
ctx.closeMenu();
}}
table={ctx.table}
/>
{/snippet}
{#snippet createButton()}
<Button onclick={() => table.setCreatingRow(true)}>Create new user</Button>
{/snippet}
{#if Object.values(validationErrors).some(Boolean)}
<p role="alert" style="color: tomato">
{Object.values(validationErrors).filter(Boolean).join(' · ')}
</p>
{/if}
<ShadcnSvelteTable {table} />