Expanding
Expanding
Expanding shows and hides extra rows below a row. It covers two things at once: opening a parent row to reveal its child rows, and opening any row to reveal a detail panel of your own making. This page ports TanStack Table's Expanding Feature (React) Guide.
Expanding is the fifth stage of the row model pipeline.
The expanded stage inserts the sub-rows of every expanded row into the row
list, so what comes out is a flat list you can render one tr at a time.
Group rows produced by Grouping are ordinary expandable
rows, so the same toggle opens and closes a group.
Sub-rows as expanded data
Sub-rows come from your data. Tell the core row model how to reach a row's
children with
withSubRows, and every row gets its
children as sub-rows all the way down:
config : Table.Config Node
config =
Table.config columns
|> Table.withSubRows (nodeFields >> .children)
|> Table.withGetRowId (\node _ _ -> (nodeFields node).id)
withSubRows runs for every row and every sub-row, so keep it cheap. Elm has
no recursive type alias, so a nested record needs a custom type:
nodeFields : Node -> { id : String, name : String, salary : Float, children : List Node }
nodeFields (Node fields) =
fields
A detail panel instead of sub-rows
getCanExpand is False for a row
with no sub-rows. Override it with
withRowCanExpand when a row's
expanded content is not table rows at all but something you render yourself:
detailConfig : Table.Config Person
detailConfig =
Table.config
[ Table.column "firstName" (.firstName >> Value.String)
, Table.column "lastName" (.lastName >> Value.String)
]
|> Table.withGetRowId (\person _ _ -> person.id)
|> Table.withRowCanExpand (\_ -> True)
Then render the panel as a second tr keyed off
getIsExpanded. The package has
nothing to do with what goes inside it:
viewRowAndPanel : Table.State -> Table.Row Person -> List (Html Msg)
viewRowAndPanel state row =
let
cells : List Table.Cell
cells =
Table.visibleCells detailConfig state row
in
Html.tr [ onClick (ExpandToggled (Table.rowId row)) ]
(List.map (\cell -> Html.td [] [ Html.text (Value.toString cell.value) ]) cells)
:: (if Table.getIsExpanded detailConfig state row then
[ Html.tr []
[ Html.td [ colspan (List.length cells) ]
[ Html.text ("Anything you like about " ++ (Table.rowOriginal row).firstName) ]
]
]
else
[]
)
State
Expanding owns one state slice. Expanded is an abstract type with two
cases, standing in for TanStack's true | Record<string, boolean>:
-- in Table.State
expanded : Expanded
-- in Table.initialState
expanded = Table.expandedIds Set.empty
| Value | Meaning |
|---|---|
expandAll |
Every row is expanded. TanStack's expanded: true. |
expandedIds |
Only the rows whose ids are in this Set String are expanded. |
Read the slice back with
expandedIdsOf, which is Nothing
for expandAll:
expandedRowIds : Table.State -> Maybe (Set String)
expandedRowIds state =
Table.expandedIdsOf state.expanded
Writing the slice directly is
setExpanded:
expandEverything : Table.State -> Table.State
expandEverything =
Table.setExpanded Table.expandAll
expandOnly : List String -> Table.State -> Table.State
expandOnly rowIds =
Table.setExpanded (Table.expandedIds (Set.fromList rowIds))
Config options
| Option | Type | Default | Description |
|---|---|---|---|
enableExpanding |
Bool |
True |
Turns expanding off for the whole table. |
getSubRows |
row -> List row |
\_ -> [] |
Where a row's children come from. Set it with withSubRows. |
getRowCanExpand |
Maybe (Row row -> Bool) |
Nothing |
A per-row override for "can this row expand?". Set it with withRowCanExpand. It wins over enableExpanding and over the "has sub-rows" rule. |
getIsRowExpanded |
Maybe (Row row -> Bool) |
Nothing |
A per-row override for "is this row expanded?". Set it with withIsRowExpanded. It wins over State.expanded outright. |
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 Pagination. |
manualExpanding |
Bool |
False |
Skip the expanded stage. expandedRowModel returns its input unchanged, and the rows you pass in are assumed to be expanded already. |
With paginateExpandedRows = False the expanded stage is a pass-through and
the sub-rows are inserted by the paginated stage instead, after the page has
been sliced. That is what puts a row's descendants on the same page as the
row, and it is why
rowsInDisplayOrder, not the
page's .rows, is what you render. The one exception is
manualPagination = True: with no page slice to run in, the expansion goes
back to the expanded stage, matching TanStack.
Two options on the filtering side change which rows survive to be expanded:
filterFromLeafRows makes a parent survive when any descendant matches, and
maxLeafRowFilterDepth bounds how deep that search goes. Both are described
in Column Filtering.
Column options
Expanding has no column builders. Sub-rows are a property of the data, so
they are configured on the Config with
withSubRows, not per column.
Transitions
| Function | Signature | Description |
|---|---|---|
toggleExpanded |
Config row -> RowModel row -> Row row -> Maybe Bool -> State -> State |
Expand or collapse one row. Nothing toggles it. |
toggleAllRowsExpanded |
Config row -> RowModel row -> Maybe Bool -> State -> State |
Expand or collapse every row. Nothing toggles on getIsAllRowsExpanded. |
setExpanded |
Expanded -> State -> State |
Replace the slice. |
resetExpanded |
State -> State |
Collapse everything, back to no ids. |
toggleExpanded takes a RowModel because it has to materialise
expandAll into the ids of the rows
that can expand before it can collapse one of them. Give it the
pre-expanded model, which is the sorted one:
preExpanded : Table.State -> Table.RowModel Node
preExpanded state =
Table.coreRowModelFromList config state nodes
|> Table.filteredRowModel config state
|> Table.groupedRowModel config state
|> Table.preExpandedRowModel config state
update : Table.RowModel Node -> Msg -> Table.State -> Table.State
update model msg state =
case msg of
ExpandToggled rowId ->
case Table.findRow model rowId of
Just row ->
Table.toggleExpanded config model row Nothing state
Nothing ->
state
AllExpandedToggled ->
Table.toggleAllRowsExpanded config model Nothing state
ExpandedReset ->
Table.resetExpanded state
Expanding a row that cannot expand is a no-op, and so is any request that matches the current state. Collapsing always applies, so a stale id can be cleaned up.
Queries
| Function | Signature | Description |
|---|---|---|
getCanExpand |
Config row -> Row row -> Bool |
Can this row expand? |
getIsExpanded |
Config row -> State -> Row row -> Bool |
Is this row expanded? |
getIsAllParentsExpanded |
Config row -> State -> RowModel row -> Row row -> Bool |
Is every ancestor of this row expanded? The row itself is not considered. |
getCanSomeRowsExpand |
Config row -> RowModel row -> Bool |
Can any row of this model expand? |
getIsSomeRowsExpanded |
State -> Bool |
Is any row expanded? expandAll counts. |
getIsAllRowsExpanded |
Config row -> State -> RowModel row -> Bool |
Is every expandable row expanded? An empty slice is False. |
getExpandedDepth |
Config row -> State -> RowModel row -> Int |
The deepest expanded row id, counted in .-separated segments. |
preExpandedRowModel |
Config row -> State -> RowModel row -> RowModel row |
The model expansion runs on, which is the sorted one. |
The package adds no toggle UI. Write the button yourself and hide it when the row cannot expand:
viewExpander : Table.State -> Table.Row Node -> Html Msg
viewExpander state row =
if Table.getCanExpand config row then
Html.button [ onClick (ExpandToggled (Table.rowId row)) ]
[ Html.text
(if Table.getIsExpanded config state row then
"-"
else
"+"
)
]
else
Html.text ""
The table-wide controls read the same way:
viewToolbar : Table.State -> Table.RowModel Node -> Html Msg
viewToolbar state model =
Html.div []
[ Html.button [ onClick AllExpandedToggled ]
[ Html.text
(if Table.getIsAllRowsExpanded config state model then
"Collapse all"
else
"Expand all"
)
]
, Html.button [ onClick ExpandedReset ] [ Html.text "Reset" ]
, Html.text
(String.fromInt (Table.getExpandedDepth config state model) ++ " levels open")
]
TanStack reads the pre-pagination row model for
getCanSomeRowsExpand so a
control can reflect rows that are not on the current page. Give it the same
model.
Not covered
Controlled state through the atoms option or state.expanded plus
onExpandedChange has nothing to port: State.expanded is always yours.
row.getToggleExpandedHandler and table.getToggleAllRowsExpandedHandler are
not ported either, because the package produces no event handlers.
autoResetExpanded and autoResetAll are not ported. Nothing recomputes
behind your back, so collapse rows in the same update branch that changes
the data or the grouping, if that is what you want.
Pinning and sorting expanded rows need nothing extra: they behave exactly as described in Row Pinning and Sorting.
Example
Expanding, ported from TanStack's Expanding example. For the detail-panel use, see Expanding Sub Components.