Files
emaui/libs/ui/src/lib/data/AdvancedTable.md
estifanos 18de7b1c1d UI fixes
2026-08-11 11:39:46 +00:00

5.8 KiB

AdvancedTable

Server-paginated data table with a column-visibility ("View") menu. Built on Mantine Table. Portable — two files, no app-specific imports.

Files

  • AdvancedTable.tsx — the component.
  • useServerTable.ts — small hook for page-index + search-query state (optional, but pairs with it).

To use in another project, copy both files as-is into that project and export them from your UI barrel (or import by relative path).

Dependencies

@mantine/core       (tested on ^8)
@tabler/icons-react  (IconRefresh, IconEye, IconInbox)
react-i18next        (useTranslation)
react                (>=17, uses hooks)

If the target project doesn't use react-i18next, replace the t(key, fallback) calls with plain strings — the component only reads the fallback text, translation is not load-bearing.

Install

npm install @mantine/core @tabler/icons-react react-i18next

Mantine must already be set up with MantineProvider in the app root — this component does not wrap one.

Copy the source

Copy AdvancedTable.tsx and useServerTable.ts into the new project (e.g. src/components/table/). No modifications needed unless you're renaming the i18n keys.

API

AdvancedColumn<T>

Field Type Required Notes
header ReactNode yes Column heading, also used as the label in the View menu.
accessorKey string no Dot-path into the row (e.g. "expectation.name"), used when cell is omitted.
cell (ctx: { row: { original: T }; value: unknown }) => ReactNode no Custom cell renderer. Takes priority over accessorKey.
size number no Column width in px.
align 'left' | 'center' | 'right' no Text alignment for header + cells.
enabled boolean no Whether the column starts visible. Default true. Toggled at runtime via the View menu.

AdvancedTable<T> props

Prop Type Required Notes
columns AdvancedColumn<T>[] yes
data T[] yes Rows for the current page only — not the full dataset.
tableName string yes Shown as the table title.
itemCount number yes Total row count on the server. Drives pagination and the count badge — not data.length.
pageIndex number yes 0-based current page.
onPageChange (pageIndex: number) => void yes
pageSize number no Default 10. Pagination only renders when itemCount > pageSize.
onPageSizeChange (pageSize: number) => void no Shows a page-size <Select> (10/20/30/40/50 by default) next to the pagination when given.
pageSizeOptions number[] no Options for the page-size select. Default [10, 20, 30, 40, 50].
refresh () => void no Shows a Refresh button when provided.
onSearchChange (q: string) => void no Reserved for a search box; not currently rendered by the component itself (wire your own input and call this, or drive useServerTable's setQ).
isLoading boolean no Shows a loader row; also spins the Refresh button.
emptyText string no Message when data is empty.

Rows should have an id: string \| number field — used as the React key (falls back to row index if absent).

useServerTable(opts?)

const { pageIndex, setPageIndex, q, setQ, pageSize, setPageSize, skip, take } = useServerTable({ pageSize: 10 });

Centralizes page-index + search-query state for a server-paginated list. setQ and setPageSize both reset pageIndex back to 0. skip/take are ready to drop into an offset-based API call. Wire setPageSize into AdvancedTable's onPageSizeChange to expose the page-size select.

Behavior notes

  • Column visibility is local UI state (useState), re-initialized from each column's enabled on mount — it does not persist across reloads or sync back to the caller.
  • At least one column always stays visible; the View menu disables unchecking the last one.
  • The View menu closes only via outside click (closeOnItemClick={false}), so multiple columns can be toggled per open.

Usage

Basic

import { AdvancedTable, type AdvancedColumn } from './table/AdvancedTable';

interface User {
  id: string;
  name: string;
  email: string;
}

const columns: AdvancedColumn<User>[] = [
  { header: 'Name', accessorKey: 'name' },
  { header: 'Email', accessorKey: 'email', enabled: true },
];

<AdvancedTable
  columns={columns}
  data={users}
  tableName="Users"
  itemCount={users.length}
  pageIndex={0}
  onPageChange={() => {}}
/>

Server-paginated, with refresh and a custom action column

import { AdvancedTable, useServerTable, type AdvancedColumn } from './table/AdvancedTable';

function UsersTable() {
  const { pageIndex, setPageIndex, skip, take } = useServerTable({ pageSize: 10 });
  const { data, isFetching, refetch } = useGetUsersQuery({ skip, take });

  const users = data?.items ?? [];
  const totalCount = data?.total ?? 0;

  const columns: AdvancedColumn<User>[] = [
    { header: 'Name', accessorKey: 'name' },
    { header: 'Email', accessorKey: 'email' },
    {
      header: 'Status',
      align: 'center',
      cell: ({ row }) => (row.original.active ? 'Active' : 'Inactive'),
    },
    {
      header: '',
      size: 80,
      cell: ({ row }) => (
        <ActionIcon onClick={() => onEdit(row.original)}>
          <IconEdit size={14} />
        </ActionIcon>
      ),
    },
  ];

  return (
    <AdvancedTable
      columns={columns}
      data={users}
      tableName="Users"
      itemCount={totalCount}
      pageIndex={pageIndex}
      onPageChange={setPageIndex}
      pageSize={10}
      refresh={refetch}
      isLoading={isFetching}
      emptyText="No users found"
    />
  );
}

Real reference implementation: apps/backoffice/src/app/features/configuration/pages/ConfigurationPage.tsx (ProfessionTab).