Data table
Search, filters, column visibility, pagination, selection, row actions, inline and multi-record editing and save hooks with toasts, all built from fandry-* primitives. It is a working screen, not a screenshot: everything below runs against a fake backend with real latency and real validation.
Try it: choose Edit from a row's menu (or double-click a cell), clear the name or enter an email without an @, then Save. Select two or more rows for Edit selected.
Install
Link to section InstallLWR / LWC OSS
npm install fandryui <!-- nothing to copy: your bundler ships only what you use --> <fandry-data-table></fandry-data-table>
Salesforce DX
npx fandry add data-table # adds the block and everything it is built from # to your package directory, as source you own
Usage
Link to section Usage<fandry-data-table
columns={columns}
data={rows}
loading={loading}
page-size="8"
save-row={saveRow}
save-rows={saveRows}
delete-row={deleteRow}
onrowsave={handleRowSave}
onrowdelete={handleRowDelete}
></fandry-data-table>
A column is a plain tanstack column definition. What makes it filterable, editable or badged is its meta:
columns = [
{ id: 'name', accessorKey: 'name', header: 'Name',
meta: { editor: { type: 'text' } } },
{ id: 'role', accessorKey: 'role', header: 'Role',
meta: {
filter: { options: ROLE_OPTIONS },
editor: { type: 'select', options: ROLE_OPTIONS }
} },
{ id: 'status', accessorKey: 'status', header: 'Status',
meta: {
filter: { options: STATUS_OPTIONS },
editor: { type: 'select', options: STATUS_OPTIONS },
badges: { active: 'success', pending: 'warning', suspended: 'danger' }
} }
];
The block owns the workflow (saving state, toasts, failure handling); you own the request and the data:
// The hook is where the request goes. Resolve with the saved row, or throw
// to keep the row in edit mode and show the message in a toast.
saveRow = (row, changes) => api.update(row.id, changes);
saveRows = (rows, changes) => api.updateMany(rows.map((r) => r.id), changes);
deleteRow = (row) => api.remove([row.id]);
// The block never edits `data` itself -- put the result back. A multi-record
// edit fires rowsave once per saved row, so this one handler covers both.
handleRowSave(event) {
const { row } = event.detail;
this.rows = this.rows.map((r) => (r.id === row.id ? row : r));
}
handleRowDelete(event) {
this.rows = this.rows.filter((r) => r.id !== event.detail.id);
}
Props
Link to section Props| Prop | Type | Default | Description |
|---|---|---|---|
columns | ColumnDef[] | [] | Plain tanstack column definitions. Filters, editors and badges are read from each column's meta. |
data | object[] | [] | The rows. Controlled: the block never changes it. |
get-row-id | function | row => String(row.id) | Stable row identity, used by selection and editing. |
get-row-label | function | — | A readable name for a row (row => row.name), used in toasts, the delete prompt and aria-labels. |
messages | object | {} | Everything the block says, from button labels to toasts, e.g. to translate it: any key of DEFAULT_DATA_TABLE_MESSAGES (fandry/dataTableState), fandry-table's own included. Keys you leave out keep their English default. |
loading | boolean | false | Renders skeleton rows in place of the data. |
save-row | (row, changes) => Promise | — | Persists an inline edit. changes holds only the changed columns. Reject to keep the row editing and toast the error. |
save-rows | (rows, changes) => Promise | — | Persists a multi-record edit as one batch (select two or more rows, then Edit selected). changes holds only the fields the user filled in. Resolve with the saved rows or nothing; reject to keep the dialog open and toast the error. |
delete-row | (row) => Promise | — | Same contract, for the built-in Delete action (after the confirmation). |
row-actions | { value, label }[] | Edit, Delete | The row menu. edit and delete are built in, and without a label take theirs from messages; any other value fires rowaction. An empty list removes the column. |
column-visibility | { [columnId]: boolean } | {} | Which columns are hidden ({ email: false }). Set it to start hidden or to restore a saved choice; the block keeps it current as the user toggles columns. |
page-size | number | 10 | Rows per page. |
toast-duration | number | 4000 | Milliseconds before a toast dismisses itself. |
showToast(variant, message) | method | — | Shows a toast, for actions you run yourself (a bulk delete, say). |
Events
Link to section Events| Event | detail | Description |
|---|---|---|
rowsave | { id, row, changes } | A save succeeded. Put row into your data. A multi-record edit fires it once per saved row. |
rowdelete | { id, row } | A delete succeeded. Take the row out of your data. |
rowaction | { action, id, row } | A custom row-menu action was chosen. |
rowselectionchange | { rowSelection, rows } | The selection changed (inherited from fandry-table). |
columnvisibilitychange | { columnVisibility } | A column was shown or hidden. Save it if you want it remembered. |
filterchange | { globalFilter, columnFilters } | Search or a filter changed. |
sortchange · pagechange | Inherited from fandry-table. |
Slots
Link to section Slots| Slot | Description |
|---|---|
toolbar | Controls at the end of the toolbar -- Refresh, Add, bulk actions. |
search | Replaces the search box. Dispatch a bubbling input event with .value. |
empty | What shows when nothing matches. |
pagination | Replaces the pagination control. |
selection-all | Replaces the select-all checkbox. |
Make it yours
Link to section Make it yoursThe block is two files on purpose. fandry/dataTableState has all the behavior and no template; fandry/dataTable is that class plus the markup you see here. To change the markup, extend the state class and write your own template, or, on Salesforce, just edit the copy fandry add put in your project. Built from:tableinputselectcheckboxbuttonbadgepopovermenudialogtoastskeletonspinnerpagination