Pagination
Pagination
Pagination slices the rows into pages and hands you one page at a time. It is the last stage of the row model pipeline, so it sees rows that are already filtered, grouped, sorted, and expanded. This page ports TanStack Table's Pagination (React) Guide.
The same state slice and the same transitions work whether the slicing is
done here or by your server. Client-side pagination runs
paginatedRowModel;
server-side pagination sets manualPagination = True, which turns that stage
into a pass-through, and tells the table how many rows there are with
rowCount or pageCount.
Client-side or server-side
Client-side pagination is the simpler option whenever the browser can hold the whole dataset. Move to server-side pagination when querying, transferring, or holding all the rows is too expensive. Row count alone does not decide it; see Client-Side vs Server-Side.
Virtualization is a different thing again: it renders fewer rows without reducing how many rows the browser holds, so it complements pagination rather than replacing it. See Virtualization.
State
Pagination owns one state slice, a record of two Ints:
-- in Table.State
pagination : Pagination
type alias Pagination =
{ pageIndex : Int
, pageSize : Int
}
-- in Table.initialState
pagination = { pageIndex = 0, pageSize = 10 }
pageIndex is zero-based, so the first page is 0. To start somewhere else,
write the slice before you store the state:
startOnPageThree : Table.State
startOnPageThree =
Table.setPagination { pageIndex = 2, pageSize = 25 } Table.initialState
unlimitedPageSize is the page
size that puts every row on one page. Elm has no Infinity for Int, so it
is Number.MAX_SAFE_INTEGER where TanStack writes pageSize: Infinity:
showEveryRow : Table.State -> Table.State
showEveryRow =
Table.setPageSize Table.unlimitedPageSize
Config options
| Option | Type | Default | Description |
|---|---|---|---|
manualPagination |
Bool |
False |
Skip the paginated stage. The data you pass in is assumed to be one page already. |
pageCount |
Maybe Int |
Nothing |
How many pages there are in total. Just -1 means the count is unknown. |
rowCount |
Maybe Int |
Nothing |
How many rows there are in total. The page count is derived from it and the page size when pageCount is not set. |
paginateExpandedRows |
Bool |
True |
With it False, expanded sub-rows always render on their parent's page, so a page can hold more rows than the page size. See Expanding. |
Config is a plain record, so a server-side table sets these with a record
update:
serverConfig : Int -> Table.Config Person
serverConfig totalRows =
let
base : Table.Config Person
base =
Table.config columns
in
{ base | manualPagination = True, rowCount = Just totalRows }
With an unknown page count (pageCount = Just -1),
getCanNextPage is always True
because the end cannot be detected,
getCanPreviousPage still
depends on the page index, and
getCanLastPage is False because
there is no finite last page to jump to.
Column options
Pagination has no column builders. It is a whole-table feature.
Transitions
| Function | Signature | Description |
|---|---|---|
setPage |
Config row -> Int -> State -> State |
Go to a page. TanStack's setPageIndex. Clamped to [0, pageCount - 1] when Config.pageCount is set; a pageCount of Just -1 clamps nothing. |
setPageSize |
Int -> State -> State |
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. |
setPagination |
Pagination -> State -> State |
Replace the whole slice. |
previousPage |
Config row -> State -> State |
Back one page, clamped at 0. |
nextPage |
Config row -> State -> State |
Forward one page. |
firstPage |
Config row -> State -> State |
Go to page 0. |
lastPage |
Config row -> State -> RowModel row -> State |
Go to the last page. A no-op when the page count is unknown or empty. |
resetPageIndex |
Config row -> State -> State |
Back to page 0. |
resetPageSize |
State -> State |
Back to a page size of 10. |
resetPagination |
State -> State |
Back to page 0 with a page size of 10. |
lastPage is the only transition that
needs a row model, because it has to know the page count. Every other one
works from the Config and the State alone.
update : Msg -> Table.State -> Table.State
update msg state =
case msg of
FirstPageClicked ->
Table.firstPage config state
PreviousPageClicked ->
Table.previousPage config state
NextPageClicked ->
Table.nextPage config state
LastPageClicked ->
Table.lastPage config state (prePagination state)
PageEntered typed ->
case String.toInt typed of
Just wanted ->
Table.setPage config (wanted - 1) state
Nothing ->
state
PageSizeChanged typed ->
Table.setPageSize (Maybe.withDefault 10 (String.toInt typed)) state
Queries
| Function | Signature | Description |
|---|---|---|
getPageCount |
Config row -> State -> RowModel row -> Int |
Config.pageCount when set, otherwise the row count divided by the page size, rounded up. |
getPageOptions |
Config row -> State -> RowModel row -> List Int |
Every page index, [0, 1, ...]. Empty when there are no pages. |
getRowCount |
Config row -> RowModel row -> Int |
Config.rowCount when set, otherwise the rows of the model handed in. |
getCanPreviousPage |
State -> Bool |
Is there a page before this one? |
getCanNextPage |
Config row -> State -> RowModel row -> Bool |
Is there a page after this one? An unknown page count always says yes. |
getCanLastPage |
Config row -> State -> RowModel row -> Bool |
Is there a known last page after this one? |
prePaginationRowModel |
Config row -> State -> RowModel row -> RowModel row |
The model pagination slices, which is the expanded one. |
rowsInDisplayOrder |
Config row -> State -> RowModel row -> List (Row row) |
The rows to render, in order. |
displayIndex |
Config row -> State -> RowModel row -> Row row -> Int |
A row's zero-based position in that list, or -1. |
Give the counts the pre-pagination model
getRowCount,
getPageCount,
getPageOptions,
getCanNextPage, and
getCanLastPage all count rows.
Pass them the pre-pagination row model, not the page. Passing the page
gives you a row count of at most one page and a page count of 1, and the
Next button will be disabled on every page.
prePagination : Table.State -> Table.RowModel Person
prePagination state =
Table.coreRowModelFromList config state people
|> Table.filteredRowModel config state
|> Table.groupedRowModel config state
|> Table.sortedRowModel config state
|> Table.prePaginationRowModel config state
The page itself is one more call on top of that:
page : Table.State -> Table.RowModel Person
page state =
Table.paginatedRowModel config state (prePagination state)
Rows come out of the page through
rowsInDisplayOrder. With
paginateExpandedRows = False that function is where the expanded
descendants missing from the page slice are put back, so use it rather than
reading .rows directly.
A pagination control bar
Everything above together: first, previous, next, last, a page readout, a "go to page" input, a page size select, and a total row count.
viewPager : Table.State -> Html Msg
viewPager state =
let
before : Table.RowModel Person
before =
prePagination state
in
Html.div []
[ Html.button
[ onClick FirstPageClicked, disabled (not (Table.getCanPreviousPage state)) ]
[ Html.text "<<" ]
, Html.button
[ onClick PreviousPageClicked, disabled (not (Table.getCanPreviousPage state)) ]
[ Html.text "<" ]
, Html.span []
[ Html.text
("Page "
++ String.fromInt (state.pagination.pageIndex + 1)
++ " of "
++ String.fromInt (Table.getPageCount config state before)
)
]
, Html.button
[ onClick NextPageClicked, disabled (not (Table.getCanNextPage config state before)) ]
[ Html.text ">" ]
, Html.button
[ onClick LastPageClicked, disabled (not (Table.getCanLastPage config state before)) ]
[ Html.text ">>" ]
, Html.input
[ Html.Attributes.type_ "number"
, value (String.fromInt (state.pagination.pageIndex + 1))
, onInput PageEntered
]
[]
, Html.select [ onInput PageSizeChanged ]
(List.map (viewPageSizeOption state) [ 10, 20, 30, 40, 50 ])
, Html.span []
[ Html.text (String.fromInt (Table.getRowCount config before) ++ " rows") ]
]
viewPageSizeOption : Table.State -> Int -> Html Msg
viewPageSizeOption state size =
Html.option
[ value (String.fromInt size), selected (state.pagination.pageSize == size) ]
[ Html.text (String.fromInt size ++ " per page") ]
For a page-number dropdown rather than a number input, build it from
getPageOptions:
viewPageJump : Table.State -> Html Msg
viewPageJump state =
Html.select [ onInput PageEntered ]
(List.map
(\index ->
Html.option
[ value (String.fromInt (index + 1))
, selected (index == state.pagination.pageIndex)
]
[ Html.text (String.fromInt (index + 1)) ]
)
(Table.getPageOptions config state (prePagination state))
)
Not covered
Controlled state through the atoms option or state.pagination plus
onPaginationChange has nothing to port: State.pagination is always yours,
so it can go straight into a query key. Nor is there an initialState option
to set a starting page; write the slice on
initialState instead, as shown
above.
autoResetPageIndex and autoResetAll are not ported. Nothing recomputes
behind your back here, so call
resetPageIndex in the same
update branch that changes a filter, the sorting, the grouping, or the
data.
TanStack's page also walks through two TanStack Query integrations,
page-index and cursor-based. The table-side settings they describe are the
manualPagination, rowCount, and pageCount options above; the request
lifecycle is your own Cmd.
Example
Pagination, ported from TanStack's Pagination example.