Skip to content

Basics

Loading example…

Every interaction the table performs goes through a named handler, and you can wrap any of them. A handler receives the intent and a next function that runs the default:

type Handler<Ctx> = (ctx: Ctx, next: (override?: Partial<Ctx>) => void | Promise<void>) => void;

Three things you can do with next, which is the whole model:

handlers={{
  // Observe, then proceed
  onSortToggle: (ctx, next) => { track('sort', ctx.column.id); next(); },

  // Change the intent: always pin to the left, whichever side was chosen
  onColumnPin: (ctx, next) => next({ position: 'left' }),

  // Cancel: a row click navigates instead of selecting
  onRowClick: ({ row }) => navigate('/people/' + row.id),
}}

Not calling next cancels the default. That is how a row click becomes navigation without turning selection off and rebuilding it.

Asking first

A handler may be async, so a confirmation is a normal await:

handlers={{
  onPageChange: async (ctx, next) => {
    if (!dirty || (await confirm('Discard unsaved changes?'))) next();
  },
}}

What can be wrapped

Sorting, search input, column filters, clearing filters, page and page-size changes, row clicks and double-clicks, row selection and select-all, expansion, column resize, move, pin and hide, export, refresh, hotkeys and cell activation. The full list with each context object is on Handlers.

They compose

TableDefaultsProvider handlers wrap the library's defaults, and a handler passed to a table wraps the provider's. Analytics can live at the provider and a screen-specific rule at the table, and both run.

Customization

Handlers are for behaviour. Appearance is slots and tokens; content is a cell renderer. Keeping them apart is what lets a handler survive a redesign.

Limitations

  • Calling next twice runs the default twice. The middleware does not deduplicate.
  • A cancelled interaction leaves no trace. If a reader needs to know why their click did nothing, say so; the table will not.
  • A handler cannot invent an interaction. It wraps the ones that exist; anything else is your own control calling the instance.

API