Data grid / foldkit-data-grid

Sixty seconds

ts
import { ColumnLayout, Columns, GridProjection, RowModel } from 'foldkit-data-grid'

interface Product {
  readonly id: string
  readonly sku: string
  readonly price: number
}

const columns = Columns.define<Product>()({
  sku: { header: 'SKU', value: product => product.sku, pinned: 'start' },
  price: { header: 'Price', value: product => product.price },
})

const products: ReadonlyArray<Product> = [
  { id: 'p1', sku: 'A-1', price: 9 },
  { id: 'p2', sku: 'B-2', price: 2 },
]
const productKey = (product: Product) => product.id

const grid = GridProjection.make({
  rows: RowModel.fromArray(products, productKey),
  columns,
  layout: ColumnLayout.initial(columns),
})

grid.columns // ['sku', 'price']
grid.moveBy({ row: 'p1', column: 'sku' }, { rows: 1, columns: 1 })
// Option.some({ row: 'p2', column: 'price' })
grid.box({ row: 'p2', column: 'price' }, { row: 'p1', column: 'sku' })
// Option.some({ rows: { start: 0, end: 2 }, columns: ['sku', 'price'] })

What each call does:

  • Columns.define<Product>()({...}) declares columns. Each key is a column's stable id, typed as a literal, and each value keeps its own type. It is called twice so the row type is given and the ids are inferred. It throws for an id that is a number, which JavaScript would enumerate first, and for __proto__.
  • RowModel.fromArray(rows, key) reads an in-memory array. It indexes the keys once per array and key function, so keep both stable across renders, and it throws when two rows share a key. A paged or cursor source implements the RowModel interface itself.
  • RowModel.map(rows, input, make) transforms each row as it is read, for local state laid over rows a server sent. make turns input (the edits, say) into the per-row function once per rows and input, and the result is the same model while both are, so the grid's projection is kept. Keys, count and order are rows' own.
  • ColumnLayout.initial(columns) is the layout the columns declare. A layout is plain data, column ids only, so it can be saved.
  • GridProjection.make(...) combines the three. It performs no I/O and holds no state; rebuild it when any input changes.

Movement and ranges

moveBy(address, { rows, columns }) steps in display order: it crosses from the pinned start into the center, skips hidden columns, and stops at the edges. rowStart, rowEnd, first and last jump to the edges. box(anchor, focus) is the rectangle two cells span, from either corner, as row indexes [start, end) and the columns it covers.

Every answer is an Option. A cell is absent when its column is hidden, its row is gone, or its row is counted but not loaded yet.