Solid Example: Aggregation

import {
  aggregationFeature,
  aggregationFn_count,
  aggregationFn_extent,
  aggregationFn_mean,
  aggregationFn_sum,
  columnFilteringFeature,
  createColumnHelper,
  createFilteredRowModel,
  createPaginatedRowModel,
  createTable,
  filterFn_includesString,
  metaHelper,
  rowPaginationFeature,
  rowSelectionFeature,
  tableFeatures,
} from '@tanstack/solid-table'
import { For, createSignal } from 'solid-js'
import { makeData } from './makeData'
import type { Sale } from './makeData'
import type { Table } from '@tanstack/solid-table'

type RowSource = 'all' | 'custom' | 'filtered' | 'page' | 'selected'
type AggregationTableMeta = { rowSource: RowSource }
const features = tableFeatures({
  aggregationFeature,
  columnFilteringFeature,
  rowPaginationFeature,
  rowSelectionFeature,
  filteredRowModel: createFilteredRowModel(),
  paginatedRowModel: createPaginatedRowModel(),
  filterFns: { includesString: filterFn_includesString },
  aggregationFns: {
    count: aggregationFn_count,
    extent: aggregationFn_extent,
    mean: aggregationFn_mean,
    sum: aggregationFn_sum,
  },
  tableMeta: metaHelper<AggregationTableMeta>(),
})
const columnHelper = createColumnHelper<typeof features, Sale>()
function getAggregationRows(
  table: Table<typeof features, Sale>,
  source = table.options.meta?.rowSource,
) {
  if (source === 'all') return table.getCoreRowModel().rows
  if (source === 'page') return table.getRowModel().rows
  if (source === 'selected') return table.getFilteredSelectedRowModel().rows
  if (source === 'custom') return table.getCoreRowModel().rows.slice(0, 3)
  return undefined
}
function formatValue(value: unknown): string {
  if (Array.isArray(value)) return value.map(formatValue).join(' – ')
  if (value && typeof value === 'object')
    return Object.entries(value)
      .map(([key, entry]) => `${key}: ${formatValue(entry)}`)
      .join(', ')
  if (typeof value === 'number')
    return value.toLocaleString(undefined, { maximumFractionDigits: 2 })
  return String(value ?? '—')
}

function App() {
  const [data, setData] = createSignal(makeData(10_000))
  const [rowSource, setRowSource] = createSignal<RowSource>('filtered')
  const columns = columnHelper.columns([
    columnHelper.display({
      id: 'select',
      header: ({ table }) => (
        <input
          type="checkbox"
          checked={table.getIsAllPageRowsSelected()}
          onChange={table.getToggleAllPageRowsSelectedHandler()}
        />
      ),
      cell: ({ row }) => (
        <input
          type="checkbox"
          checked={row.getIsSelected()}
          onChange={row.getToggleSelectedHandler()}
        />
      ),
    }),
    columnHelper.accessor('category', {
      header: 'Category',
      filterFn: 'includesString',
    }),
    columnHelper.accessor('item', {
      header: 'Item',
      footer: () => `${rowSource()} total`,
    }),
    columnHelper.accessor('amount', {
      header: 'Amount',
      aggregationFn: 'sum',
      cell: ({ getValue }) => getValue<number>().toLocaleString(),
      footer: ({ column, table }) =>
        formatValue(
          column.getAggregationValue(getAggregationRows(table, rowSource())),
        ),
    }),
    columnHelper.accessor('score', {
      header: 'Score',
      aggregationFn: [
        'count',
        'mean',
        { id: 'range', aggregationFn: 'extent' },
      ],
      footer: ({ column, table }) =>
        formatValue(
          column.getAggregationValue(getAggregationRows(table, rowSource())),
        ),
    }),
  ])
  const table = createTable({
    features,
    columns,
    get data() {
      return data()
    },
    get meta() {
      return { rowSource: rowSource() }
    },
    initialState: { pagination: { pageIndex: 0, pageSize: 10 } },
    debugTable: true,
    debugColumns: true,
  })
  const changeRowSource = (value: RowSource) => {
    setRowSource(value)
    table.setOptions((previous) => ({
      ...previous,
      meta: { rowSource: value },
    }))
  }
  const changeData = (value: Array<Sale>) => {
    setData(value)
    table.setOptions((previous) => ({ ...previous, data: value }))
  }
  return (
    <div class="demo-root">
      <h1>Aggregation without grouping</h1>
      <p>
        Amount uses a scalar <code>sum</code>. Score runs count, mean, and range
        together and returns a keyed object.
      </p>
      <div>
        <button onClick={() => changeData(makeData(10_000))}>
          Regenerate Data
        </button>
        <button onClick={() => changeData(makeData(200_000))}>
          Stress Test (200k rows)
        </button>
      </div>
      <div class="spacer-sm" />
      <div class="controls">
        <label>
          Category filter:{' '}
          <input
            value={
              (table.getColumn('category')?.getFilterValue() ?? '') as string
            }
            onInput={(event) =>
              table
                .getColumn('category')
                ?.setFilterValue(event.currentTarget.value)
            }
          />
        </label>
        <label>
          Total rows:{' '}
          <select
            value={rowSource()}
            onChange={(event) =>
              changeRowSource(event.currentTarget.value as RowSource)
            }
          >
            <option value="filtered">Filtered rows</option>
            <option value="all">All rows</option>
            <option value="page">Visible page</option>
            <option value="selected">Filtered selected rows</option>
            <option value="custom">First three core rows</option>
          </select>
        </label>
      </div>
      <div class="spacer-sm" />
      <table>
        <thead>
          <For each={table.getHeaderGroups()}>
            {(group) => (
              <tr>
                <For each={group.headers}>
                  {(header) => (
                    <th>
                      {header.isPlaceholder ? null : (
                        <table.FlexRender header={header} />
                      )}
                    </th>
                  )}
                </For>
              </tr>
            )}
          </For>
        </thead>
        <tbody>
          <For each={table.getRowModel().rows}>
            {(row) => (
              <tr>
                <For each={row.getAllCells()}>
                  {(cell) => (
                    <td
                      class={
                        cell.column.id === 'amount' ? 'numeric' : undefined
                      }
                    >
                      <table.FlexRender cell={cell} />
                    </td>
                  )}
                </For>
              </tr>
            )}
          </For>
        </tbody>
        <tfoot>
          <For each={table.getFooterGroups()}>
            {(group) => (
              <tr>
                <For each={group.headers}>
                  {(header) => (
                    <th colSpan={header.colSpan}>
                      {header.isPlaceholder ? null : header.column.id ===
                        'item' ? (
                        `${rowSource()} total`
                      ) : header.column.id === 'amount' ||
                        header.column.id === 'score' ? (
                        formatValue(
                          header.column.getAggregationValue(
                            getAggregationRows(table, rowSource()),
                          ),
                        )
                      ) : (
                        <table.FlexRender footer={header} />
                      )}
                    </th>
                  )}
                </For>
              </tr>
            )}
          </For>
        </tfoot>
      </table>
      <div class="spacer-sm" />
      <div class="controls">
        <button
          class="demo-button demo-button-sm"
          onClick={() => table.firstPage()}
          disabled={!table.getCanPreviousPage()}
        >
          {'<<'}
        </button>
        <button
          class="demo-button demo-button-sm"
          onClick={() => table.previousPage()}
          disabled={!table.getCanPreviousPage()}
        >
          {'<'}
        </button>
        <button
          class="demo-button demo-button-sm"
          onClick={() => table.nextPage()}
          disabled={!table.getCanNextPage()}
        >
          {'>'}
        </button>
        <button
          class="demo-button demo-button-sm"
          onClick={() => table.lastPage()}
          disabled={!table.getCanNextPage()}
        >
          {'>>'}
        </button>
        <span class="inline-controls">
          <div>Page</div>
          <strong>
            {(table.atoms.pagination.get().pageIndex + 1).toLocaleString()} of{' '}
            {table.getPageCount().toLocaleString()}
          </strong>
        </span>
        <span class="inline-controls">
          | Go to page:
          <input
            type="number"
            min="1"
            max={table.getPageCount()}
            value={table.atoms.pagination.get().pageIndex + 1}
            onInput={(event) =>
              table.setPageIndex(
                event.currentTarget.value
                  ? Number(event.currentTarget.value) - 1
                  : 0,
              )
            }
            class="page-size-input"
          />
        </span>
        <select
          value={table.atoms.pagination.get().pageSize}
          onChange={(event) =>
            table.setPageSize(Number(event.currentTarget.value))
          }
        >
          <For each={[10, 20, 30, 40, 50]}>
            {(size) => <option value={size}>Show {size}</option>}
          </For>
        </select>
      </div>
      <div>
        Showing {table.getRowModel().rows.length.toLocaleString()} of{' '}
        {table.getRowCount().toLocaleString()} Rows
      </div>
      <pre>{JSON.stringify(table.store.get(), null, 2)}</pre>
    </div>
  )
}
export default App