Headless table state and row-model pipeline: a port of TanStack Table core as pure functions.
There is no table instance. You own the State, you own the data, and every
function here takes a Config row and a State and gives you a value back.
Column and header types, the builders that assemble a Config row's column
list, and the readers that walk it back.
Everything about a table that is not state: the columns, how to find row ids and sub-rows, and every feature flag.
It is a plain record, so { cfg | manualSorting = True } works for any flag
that has no builder. Later versions of this package may add fields, which is
a breaking change for code that pattern matches on the record but not for
record update.
Fields, with the defaults config sets:
columns : List (Column row)getRowId : Maybe (row -> Int -> Maybe String -> String), Nothing (index paths)getSubRows : row -> List row, nonemanualSorting, manualFiltering, manualGrouping, manualExpanding,
manualPagination : Bool, all False; True makes that stage return its inputenableSorting, enableMultiSort, enableSortingRemoval,
enableMultiRemove : Bool, all True; maxMultiSortColCount : Int, unlimited;
sortDescFirst : Maybe Bool, Nothing (automatic per column)enableFilters, enableColumnFilters, enableGlobalFilter : Bool, all True;
getColumnCanGlobalFilter : Maybe (Column row -> Bool), Nothing (strings and
numbers); filterFromLeafRows : Bool, False; maxLeafRowFilterDepth : Int,
100; globalFilterFn : Maybe FilterFn, Nothing (automatic)enableGrouping : Bool, True; groupedColumnMode : GroupedColumnMode,
groupedColumnsReorderenableExpanding : Bool, True; getRowCanExpand,
getIsRowExpanded : Maybe (Row row -> Bool), Nothing;
paginateExpandedRows : Bool, TruepageCount, rowCount : Maybe Int, Nothing (manual pagination only)enableRowSelection, enableMultiRowSelection, enableSubRowSelection,
enableRowPinning : Row row -> Bool, all always True;
keepPinnedRows : Bool, TrueenableColumnPinning, enableHiding : Bool, True;
defaultColumn : SizeDefaults, size 150, min 20, max unlimitedenableCellSpanning, enableCellSelection, enableCellRangeSelection,
enableMultiCellRangeSelection : Bool, all True;
cellSelectionFilter : Maybe (Cell -> Bool), NothingenableColumnResizing : Bool, True; columnResizeMode : ColumnResizeMode,
resizeOnEnd (TanStack's default);
columnResizeDirection : ColumnResizeDirection, resizeLtrmanualAggregation : Bool, False; autoResetAll, autoResetPageIndex,
autoResetExpanded, autoResetSorting, autoResetCellSelection : Maybe Bool,
all Nothing (TanStack's automatic rule: reset unless the matching
manual* flag is set)Every state slice the pipeline reads. Start from
initialState and update it yourself, directly or through
the transition functions in this module.
sorting : List SortColumn, in priority ordercolumnFilters : List ColumnFilterglobalFilter : Value, Null for nonegrouping : List String, column ids in grouping orderexpanded : ExpandedrowSelection : Set String, selected row idspagination : PaginationcolumnOrder : List String, empty for definition ordercolumnVisibility : Dict String Bool, missing means visiblecolumnPinning : ColumnPinningcolumnSizing : Dict String Float, missing means the column's own sizerowPinning : RowPinningcellSelection : List CellSelectionRangeOne row of a row model.
The output of a pipeline stage: the row tree, the same rows flattened depth first, and a lookup by row id.
One header cell.
One header row.
One cell of one row, computed on demand.
Which rows are expanded. ExpandAll is TanStack's expanded: true.
Where Null values land when a column is sorted.
What the leaf column list does with grouped columns.
One entry of State.sorting.
One entry of State.columnFilters.
The page the paginated row model returns.
Column ids pinned to either edge.
Row ids pinned to the top or the bottom.
The default sizing of a column, in pixels.
Elm cannot re-export the variants of a type that is declared in another module, so the three unions above are abstract here and come with one function per variant.
Every row is expanded, whatever State.rowSelection holds. TanStack's
expanded: true.
Only the rows with these ids are expanded.
The expanded row ids, or Nothing when every row is expanded.
Sort Null cell values before every other value.
Sort Null cell values after every other value. This is the default.
Sort Null cell values as if they compared -1 against anything else,
TanStack's sortUndefined: -1.
Sort Null cell values as if they compared 1 against anything else,
TanStack's sortUndefined: 1.
Move grouped columns to the front of the leaf column list.
Drop grouped columns from the leaf column list.
Leave grouped columns where they are.
A configuration for a list of columns, with TanStack's defaults for every flag.
config [ Table.column "firstName" (.firstName >> Value.String) ]The state a table starts in: nothing sorted, nothing filtered, nothing grouped, page 0 of size 10.
Give rows stable ids. The function receives the datum, its index among its siblings, and its parent's row id.
Without it, root rows are "0", "1", and children are "0.1", "0.2".
Tell the core row model how to reach a row's children.
Override the default column sizing (150, 20, 9007199254740991).
Set the filter function the global filter uses.
Decide per row whether it can be selected.
An accessor column: an id and a way to read a cell value.
Table.column "age" (.age >> toFloat >> Value.Number)A group column: an id and the columns nested under it. Group columns have no accessor and no cells, they only produce header rows.
A display column: an id, no accessor, no children.
Set the header text.
Sort this column with a built-in sort function.
Sort this column with a comparison on whole rows.
Make the first click on this column sort descending.
Invert the sort direction of this column.
Decide where Null values land when this column is sorted.
Allow or forbid sorting on this column.
Allow or forbid this column in a multi-sort.
Filter this column with a built-in filter function.
Filter this column with a predicate on whole rows.
Allow or forbid a column filter on this column.
Include or exclude this column from the global filter.
Aggregate this column's values on group rows.
Read the value this column groups by, when it differs from the accessor.
The second argument is the row's index, mirroring TanStack's
getGroupingValue(originalRow, index, row).
Read the faceting values of a row, when one cell holds several.
Allow or forbid grouping by this column.
Allow or forbid hiding this column.
Allow or forbid pinning this column.
Set this column's size in pixels.
Set this column's minimum size in pixels.
Set this column's maximum size in pixels.
The column id.
The header text, when one was set.
How deep the column sits in the column tree. Top level is 0.
The columns nested under a group column.
The id of the group column this column sits under.
The accessor, when the column has one. Group and display columns have none.
This column's size in pixels, clamped to its minimum and maximum.
This column's minimum size in pixels.
This column's maximum size in pixels.
Every column, group columns included, each group before its children.
Every leaf column, in definition order.
The leaf columns a table renders: State.columnOrder applied, hidden
columns dropped.
Find a column by id. Group columns are found too.
One column and every column below it, the column itself first.
The leaf columns below one column. A leaf column returns itself.
The header rows of a table, top row first.
Every header of every header row.
The leaf headers reachable from the top header row.
The descendants of a header, deepest first, with the header itself last.
The header id. Placeholder headers get a compound id.
The id of the column this header renders.
How many leaf columns this header spans.
How many header rows this header spans. 0 means a header above already
covers this cell.
Which header row this header belongs to, counted from 1 at the top.
The header's position in its header row.
Is this a filler header standing in for a column that has no group at this level?
How many placeholders for the same column came before this one.
The headers nested under this one.
The pipeline that turns your data into a row tree, the readers that walk one row, and the faceted values later stages sample from it.
The row id.
The row's index among its siblings.
How deep the row sits in the row tree. Root rows are 0.
The original datum this row was built from.
The row's children.
The id of the row's parent, when it has one.
The raw children Config.getSubRows returned for this row.
The column a group row groups by, when the row is a group row.
The value a group row groups by.
The leaf rows a group row was built from. Empty for ordinary rows.
The aggregated values of a group row, keyed by column id.
Read one cell value. Unknown columns and columns without an accessor give
Null.
The values faceting and grouping use for one cell. A column with
withGetUniqueValues decides them; otherwise the cell
value is wrapped in a one-item list.
Every descendant of a row, depth first. The row itself is not included.
The direct parent of a row, looked up in a row model.
The ancestors of a row, from the root down to its direct parent.
One cell per leaf column, in leaf column order. Hidden columns are included.
Look a row up by id.
The deepest row depth in a row model, counting sub-rows and group rows.
A flat model is 0; one level of sub-rows makes it 1. Useful for sizing
indentation or the header checkbox of an expanding table.
The whole pipeline: core, filtered, grouped, sorted, expanded, paginated, in that order.
A manual flag on the config skips its stage and passes the row model
through unchanged.
The List form of rows.
The untouched row model: one row per datum, sub-rows resolved, ids assigned.
The List form of coreRowModel.
Drop the rows that fail the column filters and the global filter.
Config.manualFiltering skips this stage.
Replace the rows with group rows. Config.manualGrouping skips this
stage.
Sort every level of the row tree. Config.manualSorting skips this
stage.
Flatten the expanded branches into the row list.
Config.manualExpanding skips this stage.
Keep only the rows of the current page. Config.manualPagination skips
this stage.
Every distinct value of one column with the number of rows that carry it,
in first-seen order. Pass the pre-filtered row model (usually the core row
model); the count is taken over facetedRowModel, which
applies every filter except this column's own. List cells contribute each
item.
The smallest and largest Number value of one column, or Nothing when
it has none. Pass the pre-filtered row model; like
facetedUniqueValues it looks through
facetedRowModel.
The rows a column's facets are computed from: the pre-filtered rows with every active filter applied except that column's own, so a filter UI keeps showing the values the user could switch to.
Pass the row model you handed to filteredRowModel.
Passing globalFacetKey as the column id excludes the
global filter instead of a column filter.
The column id that stands for the global filter's own facet context,
"__global__". Passing it to facetedRowModel,
facetedUniqueValues or
facetedMinMax aggregates across every globally
filterable column.
Column filters narrow a row model down to the rows that match; the global
filter runs the same comparison across every column that allows it. Every
reader that has to guess something from the data (an 'auto' filter
function, for instance) takes a RowModel row to sample, exactly like
TanStack, which samples the core or filtered row model for the same job.
Can this column carry a column filter? It needs an accessor, and neither
the column nor Config.enableColumnFilters nor Config.enableFilters may
have switched filtering off.
The filtered row model does not consult this: a State.columnFilters entry
for a column that answers False is still applied, matching TanStack.
Does State.columnFilters hold an entry for this column?
This column's current filter value, when it has one.
This column's position in State.columnFilters, or -1.
The filter function a column filters with: the one set with
withFilterFn, or the automatic choice. Nothing when the column does not
exist.
Pass the core row model; the automatic choice samples it.
The filter function 'auto' picks for a column, from the type of its
first non-null value: includesString for strings, inNumberRange for
numbers, equals for booleans, arrIncludes for lists, inDateRange for
dates, and weakEquals when every value is Null.
Should a filter value be dropped from state instead of stored? A filter
function's own rule wins; without one, Null and the empty string are
dropped.
Set one column's filter value: replaced in place when the column already
has one, appended otherwise, and removed when
shouldAutoRemoveFilter says the value is blank.
Pass the core row model: a column without an explicit filter function picks its automatic one from the first values, and that choice decides the auto-remove rule.
Replace State.columnFilters wholesale, dropping the entries of known
columns whose value should auto-remove. Pass the core row model, as for
setColumnFilter.
Clear every column filter.
Does the global filter run against this column? It needs an accessor,
Config.enableGlobalFilter and Config.enableFilters have to be on, the
column must not opt out, and Config.getColumnCanGlobalFilter (whose default
keeps a column only when its first non-null value is a string or a number)
has to agree.
The filter function the global filter uses: Config.globalFilterFn, or
globalAutoFilterFn.
The global filter's automatic function: Table.FilterFn.includesString.
Set the global filter value.
Clear the global filter.
One or more columns order the rows; withSortDescFirst, withInvertSorting,
and withSortUndefined tune how a single column compares.
A sort direction.
Ascending.
Descending.
Can this column be sorted? It needs an accessor and both the column and
Config.enableSorting have to allow it.
Can this column join a multi-sort? The column's own setting wins over
Config.enableMultiSort.
This column's sort direction, or Nothing when it is not sorted.
This column's position in State.sorting, or -1.
The sort function 'auto' picks for a column. The first ten rows of the
row model are sampled: a date gives datetime, a string holding digits gives
alphanumeric, any other string gives text, and anything else gives
basic.
Pass the filtered row model, which is what TanStack samples.
The sort function a column sorts with: the one set with withSortFn, or
the automatic choice. A column set up with withCustomSort compares whole
rows and has no SortFn, so this reports its automatic choice while the row
model uses the custom comparison.
The direction a column starts sorting in when nothing says otherwise: the first non-null value among the first ten rows decides, strings ascending and everything else descending.
The direction the first click on a column sorts in: the column's
withSortDescFirst wins, then Config.sortDescFirst, then
getAutoSortDir.
The next step of a column's sort cycle. Nothing means the next step
removes the sort, which Config.enableSortingRemoval and (in a multi-sort)
Config.enableMultiRemove can forbid.
Step a column's sort: add it, replace the sort with it, flip its direction, or remove it.
desc = Just d sets the direction outright instead of stepping the cycle.
multi = True asks to add to the existing sort rather than replace it, which
happens only when getCanMultiSort allows it;
Config.maxMultiSortColCount caps how many columns a multi-sort keeps.
The row model is the pre-sorted one (the grouped row model, or the core
row model when nothing is grouped or filtered): the first sort direction of
a column without sortDescFirst depends on its values.
Replace State.sorting.
Remove one column from State.sorting, leaving the others in order.
Clear every sort.
Slices the pre-pagination row model into pages; Config.pageCount and
Config.rowCount stand in for a server-side count when the table is not
paginating in memory.
The row model pagination slices, which is the expanded row model. The row counts and page counts below all read it.
The rows a caller renders, in order. With
Config.paginateExpandedRows = False the expanded descendants that the
pre-pagination row model does not carry are inserted here.
A row's zero-based position in
rowsInDisplayOrder, or -1 when it is not there.
Go to a page, clamped to [0, Config.pageCount - 1] when
Config.pageCount is set. A Config.pageCount of Just -1 means the count
is unknown and clamps nothing.
Change the page size, at least 1. The page index moves so the row that
was at the top of the page stays in view.
Replace State.pagination.
Back to page 0.
Back to a page size of 10.
Back to page 0 with a page size of 10.
How many pages there are: Config.pageCount when it is set, otherwise
getRowCount divided by the page size, rounded up.
Every page index, [0, 1, ...].
How many rows pagination is slicing: Config.rowCount when it is set,
otherwise the rows of the pre-pagination row model.
Is there a page before this one?
Is there a page after this one? An unknown page count always says yes.
Is there a known last page after this one?
Go back one page, clamped at 0.
Go forward one page.
Go to page 0.
Go to the last page. A no-op when the page count is unknown or empty.
The page size that puts every row on one page. Elm has no Infinity for
Int, so this is Number.MAX_SAFE_INTEGER where TanStack writes Infinity.
The grouped row model replaces the rows with one group row per distinct
value of every column in State.grouping, recursively, and rolls up every
other column with an AggregationFn.
Group rows carry rowGroupingColumnId,
rowGroupingValue, rowLeafRows, and
rowAggregatedValues, and their ids are
"columnId:groupingValue" joined to the parent group's id with >.
Set a per-row override for "can this row expand?", TanStack's
getRowCanExpand. It wins over Config.enableExpanding and over the
"has sub-rows" rule.
Set a per-row override for "is this row expanded?", TanStack's
getIsRowExpanded. It wins over State.expanded outright.
How far below an aggregated row its aggregation looks for values.
0, the default, aggregates the rows themselves; 1 aggregates their
children. TanStack's maxAggregationDepth.
Can this column be grouped? Grouping has to be enabled on the table and
on the column, and the column needs either an accessor or a
withGetGroupingValue.
Is this column in State.grouping?
Where this column sits in State.grouping, or -1.
Add this column to State.grouping, or drop it and keep the rest in
order. TanStack's column_toggleGrouping does not check
getCanGroup either; its click handler does.
Replace State.grouping.
Empty State.grouping.
Was this row built by the grouped row model?
The value this row groups by for one column:
withGetGroupingValue when the column has one, the
cell value otherwise.
Is this the cell of the column its group row groups by? Cell carries
its row and column ids, so this takes the row and the column id.
Is this the cell of a grouped column that is not this row's own grouping column? Those cells render as placeholders.
The row model grouping runs on: the filtered one.
The aggregation function a column with no
withAggregationFn gets: sum for a numeric column,
extent for a date column, none for anything else. The kind is read off the
first flat row of the row model handed in.
The aggregation function of a column: its own, or the automatic one.
Aggregate one column over the rows of a row model, at the column's own
withMaxAggregationDepth. TanStack's
column.getAggregationValue().
Aggregate one column over a chosen row list and depth, TanStack's
column.getAggregationValue({ rows, maxDepth }). Use it for footers and
summaries that the grouped row model does not produce, for example the total
of a column over every filtered row. The row model is only there to resolve
an automatic aggregation function from the column's values; pass the core or
filtered model. maxDepth stops the descent into sub-rows; Nothing means
the column's own maxAggregationDepth.
Is this cell an aggregated one? True on a group row for a column that is neither the row's own grouping column nor itself grouped, and that has an aggregation function.
Splices the sub-rows of expanded rows back into the row list, according to
State.expanded.
The row model expansion runs on: the sorted one.
Can this row expand? withRowCanExpand wins,
otherwise Config.enableExpanding has to be on and the row needs sub-rows.
Is this row expanded? withIsRowExpanded wins,
otherwise State.expanded decides.
Is every ancestor of this row expanded? The row itself is not considered.
Can any row of this row model expand? TanStack reads the pre-pagination row model here, so controls can reflect rows that are not on this page.
Is any row expanded? True for expandAll and for a
non-empty expandedIds set; it does not check that the ids
still exist in the data, which is what TanStack's getIsSomeRowsExpanded
does too.
Is every expandable row of this row model expanded? An empty
State.expanded is False, and so is one whose ids match no expandable row.
The deepest expanded row id, counted in .-separated segments.
Expand or collapse one row. Nothing toggles it. Expanding a row that
cannot expand and any request that matches the current state are no-ops;
collapsing always applies, so a stale id can be cleaned up.
The row model materialises expandAll into the ids of the rows
that can expand before the change lands. Pass the pre-expanded row model
(the sorted row model, or whatever preExpandedRowModel
gives you), so group rows are included when grouping is on.
Expand or collapse every row. Nothing toggles on
getIsAllRowsExpanded.
Replace State.expanded.
Collapse everything: State.expanded back to no ids.
Row selection propagates to sub-rows and reports isSomeSelected and
isAllSelected per parent. Every transition is ... -> State -> State so it
pipes, and every query takes the Config and the State first, then the
row model or column it is about.
How much of a parent row's sub-tree is selected.
The options of a selection toggle. selectChildren also writes the
row's sub-tree; deselectParents drops the ancestors of a row that is being
deselected.
No selectable descendant of this row is selected.
Some, but not all, selectable descendants are selected.
Every selectable descendant is selected.
selectChildren on, deselectParents off: TanStack's defaults.
These are abstract for the same reason as the types above, so they come with one function per variant.
Select or deselect one row; Nothing flips it. Sub-rows follow along.
The row model is only read for the parent chain, so the core row model is
the usual argument.
toggleRowSelected with explicit
SelectOptions.
Select or deselect every row of the given model; Nothing flips on the
current all-selected state. Pass the filtered row model, which is what
TanStack's pre-grouped model is.
Select or deselect every row of the current page. Pass the paginated row model.
Clear the selection, ids of rows that cannot be selected included. This
is TanStack's deselectAll option.
Replace the selection.
Clear the selection.
Select or deselect every row between an anchor id and this row, in
display order. Pass the pre-pagination row model (the expanded row model):
like TanStack's getRowsInDisplayOrder, the range ignores the current page
and honours Config.paginateExpandedRows, so a shift-click can span pages.
Falls back to an ordinary toggle when the range is not usable, which is what
the shift-click handler does.
selectRange with explicit
SelectOptions.
Would a range from this anchor to this row be selected as a range? Both endpoints have to be in the display order of the pre-pagination row model and allow multi-selection.
Is this row selected?
Is anything selected at all?
Is every selectable row of the given model selected? Pass the filtered row model.
Is every selectable row of the current page selected?
Is any row of the current page selected, or partly selected?
Can this row be selected?
Can selecting this row select its sub-rows?
Can this row take part in a multi-row selection?
Is part, but not all, of this row's sub-tree selected?
Is this row's whole sub-tree selected?
How much of this row's sub-tree is selected.
The selected row ids.
Keep only the selected rows of a row model. Selected descendants of
unselected parents stay in flatRows and rowsById but not in rows,
exactly like TanStack's selectRowsFn. TanStack's three selected row models
are this function over the core, the filtered, and the sorted row model.
Column pinning returns left, center, and right leaf column lists; row pinning does the same for rows. Neither one touches the DOM.
Where a column is pinned: pinnedLeft,
pinnedRight, or columnUnpinned.
TanStack calls these 'start', 'end', and false.
Which slice of the visible leaf columns a query is about.
allColumnsRegion is TanStack's absent position
argument and means the whole visible list in table order.
Where a row is pinned: pinnedTop,
pinnedBottom, or rowUnpinned.
The options of pinRowWith: pin the row's leaf rows and
its ancestors along with it.
The two row models the pinned row lists read from. With
Config.keepPinnedRows on, a pinned row is taken from prePaginated even
when it is off the current page; with it off, only current is searched.
The three visible column slices of a pinned table.
Pinned to the left edge. TanStack's 'start'.
Pinned to the right edge. TanStack's 'end'.
Not pinned. Passing this to pinColumn unpins the column.
Every visible leaf column, in table order, with no pin partitioning.
The columns pinned to the left edge.
The columns that are not pinned.
The columns pinned to the right edge.
Pinned to the top of the table.
Pinned to the bottom of the table.
Not pinned. Passing this to pinRow unpins the row.
Pin the row alone, without its leaf rows or its ancestors.
These are abstract for the same reason as the types above, so they come with one function per variant.
Pin one column to an edge, or unpin it with
columnUnpinned. A group column pins every leaf below it.
Replace the column pinning state.
Unpin every column.
Can this column be pinned? At least one leaf below it has to allow it and
Config.enableColumnPinning has to be on.
Where is this column pinned? A group column reports the region of its first pinned leaf, left before right.
The column's position inside its pinned region. Unpinned columns give
0, matching TanStack.
Is any column pinned to either edge?
Is any column pinned to the left edge?
Is any column pinned to the right edge?
The leaf columns pinned left, in pinning-state order.
The leaf columns that are not pinned, in table order.
The leaf columns pinned right, in pinning-state order.
The leaf columns of one region, hidden columns included.
The visible leaf columns pinned left.
The visible leaf columns that are not pinned.
The visible leaf columns pinned right.
The visible leaf columns of one region.
allColumnsRegion gives
visibleLeafColumns unchanged.
The three visible column slices at once, in render order.
The header rows of the left-pinned columns.
The header rows of the unpinned columns.
The header rows of the right-pinned columns.
Every header of the left-pinned header rows.
Every header of the center header rows.
Every header of the right-pinned header rows.
The left-pinned headers that have no sub-headers.
The center headers that have no sub-headers.
The right-pinned headers that have no sub-headers.
The visible cells of one row pinned left, in pinning-state order.
The visible cells of one row whose column is not pinned.
The visible cells of one row pinned right, in pinning-state order.
Pin one row to an edge, or unpin it with rowUnpinned.
Pinning removes the row id from the other edge first, so a row is never in
both lists. Whether pinned rows are drawn from the whole data set or only
the current page is Config.keepPinnedRows, read by topRows
and bottomRows. Use pinRowWith to pin a
row's parents or children along with it.
pinRow with the leaf rows or the ancestors of the row pinned
along with it. The row model is where the ancestors are looked up.
Replace the row pinning state.
Unpin every row.
Where is this row pinned?
The row's position among the pinned rows that are actually shown, or -1
when it is not pinned.
Can this row be pinned?
Is any row pinned at either edge?
Is any row pinned to the top?
Is any row pinned to the bottom?
The rows pinned to the top, in pinning-state order.
The rows pinned to the bottom, in pinning-state order.
The rows of the current page that are not pinned.
Which columns render, in what order, and how wide each one is.
Is this column visible? A group column is visible when any leaf below it is.
Can this column be hidden? Both the column flag and Config.enableHiding
have to allow it.
Show or hide one column; Nothing flips it. A group column writes every
hideable leaf below it, because visibility is keyed by leaf column id.
Replace the whole visibility map.
Clear the visibility map, which shows every column again.
Show or hide every leaf column; Nothing flips the current state.
Columns that cannot hide stay visible.
Is every leaf column visible?
Is at least one leaf column visible?
Every column of the table, group columns included, minus the hidden ones.
The cells of one row whose column is visible: left-pinned first, then the unpinned cells in table order, then the right-pinned ones.
The visible cells of one row keyed by column id.
Replace State.columnOrder.
Drop State.columnOrder, restoring definition order.
Put a column list in table order: State.columnOrder first, unlisted
columns behind the listed ones, then the grouped-column rules.
Apply Config.groupedColumnMode to a leaf column list: move the grouped
columns to the front, remove them, or leave the list alone.
Where this column sits in one region of the visible leaf columns, or -1
when it is not in that region.
Is this the first visible column of the region?
Is this the last visible column of the region?
The rendered width of a column: the committed size from
State.columnSizing when there is one, otherwise the column's own size and
then the configured default, clamped between minSize and maxSize.
How far from the start of its region a column begins.
How far from the end of its region a column ends.
Commit one column's size.
Replace the whole sizing map.
Drop one column's committed size, leaving the other columns alone.
Drop every committed size.
The width of a header: its column's size for a leaf header, the sum of the sub-header widths for a parent header.
How far from the start of its header row a header begins. Pass the headers of the row the header belongs to.
The width of the whole table: the sum of the top header row.
The width of the left-pinned region.
The width of the unpinned region.
The width of the right-pinned region.
Dragging a column edge to a new width. The package holds the transient state of the drag and does the arithmetic; the pointer events, the handle, and the subscription are yours.
The transient state of one resize drag, State.columnResizing. It is
empty between drags and is written by the three transitions below.
{ columnSizingStart = List ( String, Float )
, deltaOffset = Maybe Float
, deltaPercentage = Maybe Float
, isResizingColumn = Maybe String
, startOffset = Maybe Float
, startSize = Maybe Float
}columnSizingStart is the width every leaf header under the dragged header
had when the drag began. TanStack's isResizingColumn: false | string is a
Maybe String here.
When a drag commits the widths it computes: resizeOnChange
on every pointer move, resizeOnEnd only when the drag ends.
Commit the new widths on every pointer move.
Commit the new widths only when the drag ends. Until then the drag shows
in State.columnResizing.deltaOffset, which is what a preview indicator
renders from.
A left-to-right layout: dragging right widens the column. TanStack's
'ltr'.
A right-to-left layout: dragging left widens the column. TanStack's
'rtl'.
The two unions are abstract here for the same reason as the ones above, so they come with one function per variant.
Allow or forbid resizing one column. Ports the column-level
enableResizing.
Can this column be resized? Both the column's
withEnableResizing and the table's
Config.enableColumnResizing default to True.
Is this column the one being dragged?
Can this header's column be resized? This is the one to ask before rendering a resize handle on a header.
Is this header's column the one being dragged?
Begin a drag on a header at the pointer's clientX. Records where the
pointer started, how wide the header was, and how wide every leaf header
under it was. A column that cannot be resized leaves the state alone.
This is the mousedown or touchstart half of TanStack's
header.getResizeHandler().
Move the drag to a new clientX. Writes deltaOffset and
deltaPercentage, and in resizeOnChange mode commits the
new widths to State.columnSizing. With no drag in progress nothing happens.
This is the mousemove or touchmove half of the handler.
End the drag: commit the widths the last
updateColumnResize computed, then clear
State.columnResizing. Call updateColumnResize first with the position of
the event that ended the drag, when it carries one.
This is the mouseup, touchend, or touchcancel half of the handler.
Replace the whole transient slice. Ports table.setColumnResizing.
Drop the transient slice back to "no drag in progress". Ports
table.resetHeaderSizeInfo(true).
The three corners of TanStack that need a caller to stand in for the table
instance: the autoReset* scheduler, the per-row filter bookkeeping the
filtered row model writes, and the aggregation options that hand a column's
value to the caller.
TanStack schedules a reset of one state slice whenever a row-model stage
recomputes: the page index goes back to 0 when the data, the filters, the
sorting or the grouping changed, the expanded rows collapse when the data,
the filters or the grouping changed, and a data change also clears the
sorting and the cell selection when those are switched on.
There is no scheduler here, so autoReset is the whole thing
as one pure step: produce your next State in update, then hand it and the
previous one over.
Apply every reset TanStack's autoReset* scheduler would schedule for one
change. Call it in update once you have the next State:
newState =
Table.autoReset config
{ previous = model.tableState
, next = nextState
, dataChanged = False
}The page index goes back to 0 when the data, the column filters, the global
filter, the sorting or the grouping changed; the expanded rows collapse when
the data, the filters or the grouping changed (the grouped stage sits
downstream of filtering, so a filter change reaches it even with no grouping
active); and a data change clears the sorting and the cell selection.
Each slice consults autoResetAll, then its own flag, then its default:
withAutoResetPageIndex defaults to on unless
Config.manualPagination is set,
withAutoResetExpanded to on unless
Config.manualExpanding is set,
withAutoResetSorting to off, and
withAutoResetCellSelection to on.
Every reset goes to the feature default, never to a remembered initial state:
there is no table.initialState here.
Force every auto-reset on or off, overriding each individual flag. Ports
autoResetAll.
Override whether autoReset returns to page 0. The default
is on unless Config.manualPagination is set. Ports autoResetPageIndex.
Override whether autoReset collapses the expanded rows.
The default is on unless Config.manualExpanding is set. Ports
autoResetExpanded.
Override whether autoReset clears the sorting on a data
change. The default is off, as in TanStack. Ports autoResetSorting.
Override whether autoReset clears the cell selection on a
data change. The default is on. Ports autoResetCellSelection.
_createFilteredRowModel writes a pass/fail flag and an optional meta value
onto every row it tests, for every filter it evaluates, and the fuzzy-filter
example then sorts by the rank its filter fn stored. The flags live on the
rows filteredRowModel returns; to read them for the
rows it dropped, run taggedRowModel, which tags the same
rows and drops nothing.
The global filter's verdict is stored under
globalFacetKey, TanStack's __global__, and its meta
under the id of the column that produced it.
The pre-filtered row model with the filter flags and the filter meta written onto every row, dropping nothing.
TanStack writes both maps onto the rows of getPreFilteredRowModel() in
place while getFilteredRowModel() runs, so they can be read there for rows
that did not survive. Rows are immutable here, so this is the function that
hands those rows back. Config.manualFiltering skips it, exactly as it skips
filteredRowModel.
Whether the row passed each filter filteredRowModel
evaluated, keyed by column id, plus globalFacetKey for
the global filter. Empty on every row model but a filtered one.
The meta each filter recorded for the row, keyed by the column id it was
evaluated for. A filter fn records meta when it was built with
Table.FilterFn.withMeta, a column when it was
built with withCustomFilterMeta.
The meta one column's filter recorded for the row. This is the rank a fuzzy filter stores and a custom sort then orders by.
Filter this column with a predicate on whole rows that also produces the
meta the filtered row model records for the row, TanStack's addMeta
callback turned into a return value.
withCustomFilter is unchanged and records no meta;
when a column carries both, this one wins.
A column can aggregate with several functions at once, each under its own
key, with a function of the whole AggregationContext
rather than of the values alone, or not at all, with the caller supplying the
value instead.
What one aggregation sees while it runs. Ports AggregationContext,
minus its column and table members: there is no table instance and the
column is fixed by the call site.
rows is the depth-selected frontier and values are that frontier's values
for columnId. subRows and subRowValues are the immediate sub-rows of a
group row and the values they already carry, and are empty for root or
caller-supplied-row aggregation, where groupingRow is Nothing too.
An aggregation that reads the whole
AggregationContext instead of only the values, which
is TanStack's AggregationFnDef.aggregate. Build one with
aggregationFnWithContext.
It lives here rather than in Table.AggregationFn because that module is
imported by the one that declares Row and so cannot mention it.
Aggregate this column with several aggregation functions at once, each
under its own key: TanStack's aggregationFn: ['count', 'mean', { id: 'range',
aggregationFn: 'extent' }].
The result is a keyed Dict, not one Value, so it is read back with
aggregationResults and
rowAggregationResults rather than with
getValue, which gives Null for such a column. A duplicated
key keeps its entry and gives Null, which is TanStack warning and keeping
the key with undefined.
Aggregate this column with a function of the whole
AggregationContext. It takes precedence over
withAggregationFn.
Supply this column's aggregation value instead of computing it, TanStack's
getAggregationValue column option. It short-circuits
aggregationValue and
aggregationValueOf, before
withManualAggregation and before any aggregation
function, and like TanStack it does not touch the grouped row model, which
aggregates through the column's aggregation functions.
TanStack lets the provider decline by returning undefined and fall back to
the local aggregation; there is no counterpart for that here, since a
provider that returns Null is TanStack's handled { value: undefined }.
The AggregationFn.custom of a fold that needs the context: the rows and
values it is folding, the immediate sub-rows and their results, and the group
row the result is for.
It is here rather than in Table.AggregationFn because that module is
imported by the one that declares Row and so cannot mention it.
Hand every column's aggregation value to the caller:
aggregationValue and
aggregationValueOf give Null for a column without
withGetAggregationValue. Ports the
manualAggregation table option; like TanStack's it leaves the grouped row
model alone.
The keyed counterpart of aggregationValue for a
column built with withAggregationFns: one result per
key. A column without the list option gives an empty Dict.
The keyed aggregation results a group row carries for one column, the
counterpart of rowAggregatedValues for a column
built with withAggregationFns.
One keyed aggregation result of a group row: the row, the column id, then the aggregation key.
Spans merge adjacent cells; cell selection tracks rectangular ranges, focus, and keyboard movement over the visible grid. Optional; ignore this section if your table does not need either.
The cell span index of the rows a caller renders. Build it with
cellSpanIndex and read it with
cellRowSpan, cellColSpan, and
cellIsCovered.
What a withSpanRowsWhen predicate is given for each candidate row. The
run is anchored: anchorRow is the row whose cell renders the merged
content, and every later row of the run is tested against it.
Allow or forbid cell spanning for the whole table. False makes every
cell report a span of 1 and builds no span index.
Turn one column off for cell spanning even when the table allows it.
Merge adjacent rows whose value for this column is equal into one
vertically spanning cell. Null never merges under this comparison; use
withSpanRowsWhen to opt in.
Decide per candidate row whether it joins the vertical run anchored at
anchorRow.
Make this column's cell span that many columns in the given row, counted in render order. A span is clamped to the end of the cell's pinned region, so it never crosses the left, center, or right boundary.
The stand-in for Infinity in a withSpanColumns
callback: "the rest of my region".
Does this column take part in cell spanning? A column opting out wins over the table option.
Build the span index of the rows a caller renders. Pass the row model you
render, which is normally paginatedRowModel; row
pinning is read off the state.
The row ids the index was built from, in render order.
The vertical runs per column id, indexed by render-order row position. Only columns with at least one run longer than one row appear; a missing column means every cell in it spans exactly one row.
How many rows this cell spans: 1 when it does not span, and 0 when a
spanning cell above covers it. Never render a 0; skip the cell instead.
How many columns this cell spans: 1 when it does not span, and 0 when
another cell's column span covers it.
Does another cell's span cover this cell? Covered cells carry no content of their own and must not be rendered.
One rectangular cell selection, stored as its two defining corners. The
anchor stays put while the focus corner moves during a shift-extend or a
drag, so the pair carries more than a normalized rectangle would. Build one
with cellRange.
How a range changes the selection the ranges before it produced.
Whether a write replaces the selection, adds a rectangle, or subtracts one.
A range resolved into inclusive display-order indexes. Rows are positions
in rowsInDisplayOrder; columns are positions in the
visible leaf columns in render order.
Which sides of a selected cell sit on the outer boundary of the selection, for drawing a spreadsheet-style outline.
One step of keyboard navigation.
The two row models cell selection reads: prePaginated fixes the
display-order indexes a range resolves against, so a range spans pages, and
current is the page a caller renders, which bounds keyboard navigation and
cell spanning. Without pagination both are the same model.
A range that adds its rectangle to the selection.
A range that subtracts its rectangle from the selection.
Replace the whole selection with this rectangle.
Add this rectangle alongside the existing ranges.
Subtract this rectangle from the existing ranges.
Move or extend one row up.
Move or extend one row down.
Move or extend one column left.
Move or extend one column right.
Allow or forbid cell selection for the whole table.
Decide per cell whether it can be selected. The predicate replaces the
boolean, exactly like TanStack's enableCellSelection in its function form.
Allow or forbid extending a cell selection into a range, which is what shift-click and drag do.
Allow or forbid adding and subtracting further rectangles, which is what ctrl-click and meta-click do.
Allow or forbid selecting the cells of one column.
A range from its two corners, taken as an inclusion:
cellRange anchorRowId anchorColumnId focusRowId focusColumnId.
Replace the whole cellSelection slice.
Drop every range. This is TanStack's resetCellSelection(table, true);
there is no separate resetCellSelection here because the feature default
is the empty list.
Select a rectangle, replacing the selection.
Select a rectangle with replace, include, or exclude semantics.
Select every selectable cell as one range.
Collapse the selection to a single cell at the given coordinates.
Start a selection at one cell, replacing whatever was selected. This is
the state half of the mousedown handler with no modifier key.
Move the active range's focus corner to this cell, keeping its anchor and
its operation. This is the state half of a shift-mousedown and of a drag's
mouseenter. With no active range, or with
withCellRangeSelection off, it selects the cell
instead.
Add a rectangle at this cell alongside the existing ranges, subtracting
instead when the cell is already selected. This is the state half of a ctrl-
or meta-mousedown. With
withMultiCellRangeSelection off, it selects
the cell instead.
Move the selection one step, collapsing it to a single cell. Columns that cannot be selected are skipped over, and a merged cell is one stop. With nothing selected this selects the first selectable cell.
Extend the active range one step, keeping its anchor fixed.
Can this cell currently be selected? A column opting out wins over the table option.
Does this cell fall inside the final positive selection?
Is this cell the active cell, the anchor of the most recent range? An exclusion's active cell is focused even though it is not selected.
0 for the focused cell and -1 otherwise, for a roving tabindex.
Which sides of this cell sit on the outer boundary of the selection. All
four are False when the cell is not selected.
The active cell: the anchor of the most recent range.
The final positive selection as disjoint, inclusive display-order index rectangles, after every include and exclude is applied. A range whose corners no longer resolve is omitted rather than clamped, so it contributes nothing while staying in state.
The merged-cell rectangles of the rendered rows, in the same index space. Selection rectangles grow to enclose these, so a merged cell is always entirely selected or entirely unselected.
The render-order index of every visible column id.
The unique ids of all selected cells, in row-major order. Cells another cell's span covers are skipped, so the ids match what renders.
How many cells are selected. A merged cell counts once.
Each final positive region's values as a row-major grid, indexed as region, then row, then column. Covered cells keep their values so the grid stays rectangular; serializing it is the caller's job.
The ids of all rows the selection intersects.
The ids of all columns the selection intersects.
The overlap of two rectangles, or Nothing when they are disjoint.
The parts of the first rectangle the second does not cover, as up to four disjoint rectangles.
Add a rectangle to a disjoint set, keeping the set disjoint.
Fuse rectangles that share a full side into one, to a fixed point.
Grow a rectangle until it fully contains every merged-cell rectangle it touches.
Run ordered include and exclude operations, giving the final positive selection as disjoint rectangles.
A column definition. Build one with
column,group, ordisplay.