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.