Skip to content

Editing and Creating

EditingCrud.svelte
<script lang="ts">
import Button, { Label } from '@smui/button';
import {
MRT_ActionMenuItem,
MaterialSvelteTable,
createMaterialSvelteTable,
type MRT_ColumnDef,
type MRT_Row,
type MRT_TableInstance,
} from 'material-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 = createMaterialSvelteTable<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)} variant="raised">
<Label>Create new user</Label>
</Button>
{/snippet}
{#if Object.values(validationErrors).some(Boolean)}
<p role="alert" style="color: tomato">
{Object.values(validationErrors).filter(Boolean).join(' · ')}
</p>
{/if}
<MaterialSvelteTable {table} />

enableEditing turns editing on (a function lets you decide per row). editDisplayMode picks how:

ModeWhat happens
modalA dialog with a field per column (default)
rowThe row’s cells become inputs, with save and cancel buttons
cellDouble-click a cell. It saves when the input loses focus or on Enter
tableEvery cell is an input all the time
customYou build the UI

Disable a column with enableEditing: false, make it a dropdown with editVariant: 'select' and editSelectOptions, or replace its input with an Edit snippet.

The table never changes your data. When the user saves, onEditingRowSave receives the typed values. Write them back, then call exitEditingMode():

onEditingRowSave: ({ exitEditingMode, row, values }) => {
data = data.map((person) => (person.id === row.id ? { ...person, ...values } : person));
exitEditingMode();
},

Keep the dialog open by returning without calling exitEditingMode() (for example after a validation error). Make this async to wait for a request, and pass isSaving in state to show a spinner on the save button. onEditingRowCancel runs when the user cancels.

In cell mode there is no save button. Use editTextFieldProps with an input$onblur handler to save a single cell.

Create a table instance, and call table.setCreatingRow(true) from a button:

<script lang="ts">
const table = createMaterialSvelteTable({
columns,
get data() { return data },
createDisplayMode: 'modal', // or 'row'
enableEditing: true,
onCreatingRowSave: ({ exitCreatingMode, values }) => {
data = [values as Person, ...data];
exitCreatingMode();
},
topToolbarCustomActions: createButton,
});
</script>
{#snippet createButton()}
<button onclick={() => table.setCreatingRow(true)}>Create new user</button>
{/snippet}
<MaterialSvelteTable {table} />

With createDisplayMode: 'row' the new row is inserted inline. Use positionCreatingRow ('top', 'bottom' or an index) to choose where.

editRowDialogContent and createRowDialogContent replace the dialog’s body. They receive internalEditCells, the cells being edited:

{#snippet editRowDialogContent({ internalEditCells, row, table })}
<h2>Edit {row.original.firstName}</h2>
{#each internalEditCells as cell (cell.id)}
<MRT_EditCellTextField {cell} {table} />
{/each}
<MRT_EditActionButtons {row} {table} variant="text" />
{/snippet}

Customize the dialog itself with editRowDialogProps and createRowDialogProps.