Fandry UI
Block

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.

Simulate server errors Simulate latency

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.

Try it with 10,000 rows (the benchmark view).

Install

Link to section Install

LWR / 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
PropTypeDefaultDescription
columnsColumnDef[][]Plain tanstack column definitions. Filters, editors and badges are read from each column's meta.
dataobject[][]The rows. Controlled: the block never changes it.
get-row-idfunctionrow => String(row.id)Stable row identity, used by selection and editing.
get-row-labelfunction—A readable name for a row (row => row.name), used in toasts, the delete prompt and aria-labels.
messagesobject{}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.
loadingbooleanfalseRenders 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, DeleteThe 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-sizenumber10Rows per page.
toast-durationnumber4000Milliseconds 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
EventdetailDescription
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 · pagechangeInherited from fandry-table.

Slots

Link to section Slots
SlotDescription
toolbarControls at the end of the toolbar -- Refresh, Add, bulk actions.
searchReplaces the search box. Dispatch a bubbling input event with .value.
emptyWhat shows when nothing matches.
paginationReplaces the pagination control.
selection-allReplaces the select-all checkbox.

Make it yours

Link to section Make it yours

The 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