Row Pinning
Row pinning keeps chosen rows in a top or a bottom region while the rest of
the rows render in the center. It is the port of TanStack Table's Row
Pinning guide and its rowPinningFeature. Nothing has to be registered:
the state slice and the functions are always there.
Pinning is a rendering split, not a pipeline stage. It does not filter or sort anything; it decides which of three lists a row is drawn in.
State
Row pinning stores row ids in two lists.
-- in Table.State
, rowPinning : RowPinning
type alias RowPinning =
{ top : List String
, bottom : List String
}
-- in Table.initialState
, rowPinning = { top = [], bottom = [] }
The order inside each list is the order the pinned rows render in, so
pinning is also a small ordering of its own. To pin rows from the start, put
the ids in the state you build your model with rather than in an
initialState table option.
Config options
| Option | Type | Default | Description |
|---|---|---|---|
enableRowPinning |
Row row -> Bool |
always True |
Can this row be pinned. |
keepPinnedRows |
Bool |
True |
Keep a pinned row visible even when filtering or pagination removed it from the current page. |
Both are set with a record update; Config is a plain record and neither has
a with* builder.
activeRowsOnly : Table.Config Person
activeRowsOnly =
{ config | enableRowPinning = \row -> (Table.rowOriginal row).active }
dropFilteredOutPins : Table.Config Person
dropFilteredOutPins =
{ config | keepPinnedRows = False }
TanStack's enableRowPinning takes boolean | ((row) => boolean); here the
function form covers both, and always False is the boolean.
Column options
None. Row pinning is decided per row, so no column builder affects it. The column-level counterpart is Column Pinning.
Transitions
| Transition | Type | What it does |
|---|---|---|
pinRow |
RowPinPosition -> Row row -> State -> State |
Pin one row to an edge, or unpin it. |
pinRowWith |
RowPinPosition -> PinRowOptions -> RowModel row -> Row row -> State -> State |
The same, pinning the row's leaf rows or its ancestors along with it. |
setRowPinning |
RowPinning -> State -> State |
Replace both lists. |
resetRowPinning |
State -> State |
Unpin everything. |
RowPinPosition is an abstract type with one value per case:
Table.pinnedTop, Table.pinnedBottom, and Table.rowUnpinned. Unpinning
is pinRow Table.rowUnpinned, which is TanStack's row.pin(false).
update : Msg -> Table.State -> Table.State
update msg state =
case msg of
ClickedPin position row ->
Table.pinRow position row state
PinRowOptions decides how much of a row's family moves with it, which
matters for grouped and expanded tables. pinRow uses the default, both
False.
type alias PinRowOptions =
{ includeLeafRows : Bool
, includeParentRows : Bool
}
-- Table.defaultPinRowOptions
{ includeLeafRows = False, includeParentRows = False }
pinRowWith takes a row model because that is where the ancestors and leaf
rows are looked up.
pinWithLeafRows : Table.Row Person -> Table.State -> Table.State
pinWithLeafRows row state =
Table.pinRowWith Table.pinnedTop
{ includeLeafRows = True, includeParentRows = False }
(pinnedSource state).prePaginated
row
state
Queries
| Query | Type | Answers |
|---|---|---|
getIsRowPinned |
State -> Row row -> RowPinPosition |
Where is this row pinned? |
getCanPinRow |
Config row -> Row row -> Bool |
Can it be pinned? |
getRowPinnedIndex |
Config row -> State -> PinnedRowsSource row -> Row row -> Int |
Its position among the pinned rows that actually render, or -1. |
isSomeRowsPinned |
State -> Bool |
Is anything pinned at either edge? |
isSomeRowsPinnedTop |
State -> Bool |
Anything pinned to the top? |
isSomeRowsPinnedBottom |
State -> Bool |
Anything pinned to the bottom? |
topRows |
Config row -> State -> PinnedRowsSource row -> List (Row row) |
The rows pinned to the top, in pinning order. |
bottomRows |
Config row -> State -> PinnedRowsSource row -> List (Row row) |
The rows pinned to the bottom. |
centerRows |
State -> RowModel row -> List (Row row) |
The rows of the current page that are not pinned. |
Why the pinned lists take two row models
PinnedRowsSource carries both the model before the page slice and the model
of the current page.
type alias PinnedRowsSource row =
{ prePaginated : RowModel row
, current : RowModel row
}
keepPinnedRows is what picks between them. With it on, the default, a
pinned row is looked up by id in prePaginated, so it keeps rendering in its
region even when a filter or the current page would have dropped it. Its
ancestors still have to be expanded for it to show. With it off, only rows
present in current.rows are drawn.
centerRows only ever reads the current page, so it takes the page model
directly.
Build both models once per update and pass the pair around.
pinnedSource : Table.State -> Table.PinnedRowsSource Person
pinnedSource state =
let
beforePaging : Table.RowModel Person
beforePaging =
Table.coreRowModelFromList config state people
|> Table.filteredRowModel config state
|> Table.sortedRowModel config state
|> Table.expandedRowModel config state
in
{ prePaginated = beforePaging
, current = Table.paginatedRowModel config state beforePaging
}
Rendering the three regions
The rendered order is top, then center, then bottom. There is no single
function that concatenates them, because a table that gives each region its
own tbody needs them apart.
displayRows : Table.State -> List (Table.Row Person)
displayRows state =
let
source : Table.PinnedRowsSource Person
source =
pinnedSource state
in
Table.topRows config state source
++ Table.centerRows state source.current
++ Table.bottomRows config state source
getRowPinnedIndex is a position within the region a row is pinned to, and
-1 when the row is not pinned or is not being rendered.
pinnedRowLabel : Table.State -> Table.Row Person -> String
pinnedRowLabel state row =
let
index : Int
index =
Table.getRowPinnedIndex config state (pinnedSource state) row
in
if index < 0 then
""
else
"pinned #" ++ String.fromInt (index + 1)
Pinning controls
getCanPinRow decides whether the controls appear at all, and
getIsRowPinned compares against a position to disable the button for the
region the row is already in.
viewPinControls : Table.State -> Table.Row Person -> Html Msg
viewPinControls state row =
if Table.getCanPinRow config row then
td []
[ pinButton "Top" Table.pinnedTop state row
, pinButton "Center" Table.rowUnpinned state row
, pinButton "Bottom" Table.pinnedBottom state row
]
else
td [] []
pinButton : String -> Table.RowPinPosition -> Table.State -> Table.Row Person -> Html Msg
pinButton label position state row =
button
[ onClick (ClickedPin position row)
, disabled (Table.getIsRowPinned state row == position)
]
[ text label ]
Not ported
onRowPinningChangeandatoms. Transitions return a newState; you store it.row.position. TanStack writes apositionfield onto the rows thatgetTopRowsandgetBottomRowsreturn. The lists here hand back the rows unchanged, and you know the region from the function you called.resetRowPinning()restoring an initial slice. There is notable.initialState, soresetRowPinningclears both lists, matching TanStack'sresetRowPinning(true). To go back to a remembered slice, callsetRowPinningwith it.
Example
Row Pinning, ported from TanStack's Row Pinning example.