Domphy

Custom Features & Plugins

@domphy/table (a port of @tanstack/table-core) supports custom features — extend the table with your own state, reducers, and methods following the same plugin architecture as built-in features (sorting, filtering, etc.).

Custom feature structure

A custom feature is a plain object with hooks that integrate with the table lifecycle:

import type { TableFeature, Table, RowData } from "@domphy/table"

interface HighlightFeature {
  highlightedRows: Set<string>
  setHighlightedRows: (ids: string[]) => void
  toggleHighlight: (id: string) => void
  getIsHighlighted: (id: string) => boolean
}

const HighlightFeature: TableFeature = {
  getInitialState: (state) => ({
    ...state,
    highlightedRows: new Set<string>(),
  }),

  getDefaultOptions: (table) => ({
    onHighlightChange: (updater) => {
      const newSet = typeof updater === "function"
        ? updater(table.getState().highlightedRows)
        : updater
      table.options.onHighlightChange?.(newSet)
    },
  }),

  createTable: (table) => {
    table.setHighlightedRows = (ids) => {
      table.options.onHighlightChange(new Set(ids))
    }

    table.toggleHighlight = (id) => {
      table.options.onHighlightChange((prev) => {
        const next = new Set(prev)
        if (next.has(id)) next.delete(id)
        else next.add(id)
        return next
      })
    }
  },

  createRow: (row, table) => {
    row.getIsHighlighted = () => table.getState().highlightedRows.has(row.id)
  },
}

Registering a custom feature

Pass features to createDomphyTable:

import { createDomphyTable } from "@domphy/table/domphy"
import { toState } from "@domphy/core"

const highlighted = toState<Set<string>>(new Set())

const table = createDomphyTable({
  _features: [HighlightFeature],
  data: () => rows,
  columns,
  state: {
    highlightedRows: highlighted.get(),
  },
  onHighlightChange: (newSet) => highlighted.set(newSet),
})

// Now table has .toggleHighlight() and rows have .getIsHighlighted()
table.toggleHighlight("row-1")

Row-level state

Custom state per row (stored in the table, not in the row data):

const ExpandedNotesFeature: TableFeature = {
  getInitialState: (state) => ({
    ...state,
    expandedNotes: {} as Record<string, string>,
  }),

  createTable: (table) => {
    table.setNote = (rowId: string, note: string) => {
      table.setState((prev) => ({
        ...prev,
        expandedNotes: { ...prev.expandedNotes, [rowId]: note },
      }))
    }

    table.getNote = (rowId: string): string => {
      return table.getState().expandedNotes[rowId] ?? ""
    }
  },

  createRow: (row, table) => {
    row.getNote = () => table.getNote(row.id)
    row.setNote = (note: string) => table.setNote(row.id, note)
  },
}

Column-level feature

Custom column options and methods:

const TooltipFeature: TableFeature = {
  createColumn: (column) => {
    column.getTooltip = () => column.columnDef.meta?.tooltip ?? ""
    column.hasTooltip = () => !!column.columnDef.meta?.tooltip
  },
}

// Usage in column definition:
columnHelper.accessor("status", {
  header: "Status",
  meta: { tooltip: "Current approval status" },
})

// Render with tooltip:
const HeaderCell = (header: Header<any, unknown>) => ({
  th: [
    { span: String(header.column.columnDef.header ?? "") },
    header.column.hasTooltip()
      ? { span: header.column.getTooltip(), $: [tooltip()] }
      : null,
  ].filter(Boolean),
})

TypeScript: augmenting table types

Extend the TanStack table type declarations to get full IntelliSense for custom features:

// types/table-extensions.d.ts
import "@domphy/table"

declare module "@domphy/table" {
  interface TableMeta<TData extends RowData> {
    updateData?: (rowIndex: number, columnId: string, value: unknown) => void
  }

  interface ColumnMeta<TData extends RowData, TValue> {
    tooltip?: string
    editable?: boolean
  }

  interface TableState {
    highlightedRows: Set<string>
    expandedNotes: Record<string, string>
  }

  interface Table<TData extends RowData> {
    toggleHighlight: (id: string) => void
    setHighlightedRows: (ids: string[]) => void
    getNote: (rowId: string) => string
    setNote: (rowId: string, note: string) => void
  }

  interface Row<TData extends RowData> {
    getIsHighlighted: () => boolean
    getNote: () => string
    setNote: (note: string) => void
  }
}

Editable cells plugin

A common plugin pattern — make cells editable inline:

const EditableCellFeature: TableFeature = {
  createTable: (table) => {
    table.updateCellData = (rowIndex: number, columnId: string, value: unknown) => {
      const updateFn = table.options.meta?.updateData
      if (updateFn) updateFn(rowIndex, columnId, value)
    }
  },
}

// In the data-source:
const tableData = toState(initialRows)

const table = createDomphyTable({
  _features: [EditableCellFeature],
  data: () => tableData.get(),
  columns: [
    columnHelper.accessor("name", {
      header: "Name",
      cell: (info) => ({
        input: null,
        type: "text",
        value: String(info.getValue() ?? ""),
        onBlur: (e: FocusEvent) => {
          info.table.updateCellData(
            info.row.index,
            info.column.id,
            (e.target as HTMLInputElement).value
          )
        },
      }),
      meta: { editable: true },
    }),
  ],
  meta: {
    updateData: (rowIndex, columnId, value) => {
      tableData.set((rows) => rows.map((row, i) =>
        i === rowIndex ? { ...row, [columnId]: value } : row
      ))
    },
  },
})