Headers
A header is one <th>. Headers are to the <thead> what
cells are to the <tbody>. This page is the port of
TanStack's Headers guide.
Header row is opaque, like Row and Column: you read it through
functions rather than fields.
Where to get headers from
Header group headers
Headers arrive in header groups, one group per header
row. headerGroup.headers is the list, left to right:
viewHeaderRow : Table.HeaderGroup Person -> Html msg
viewHeaderRow headerGroup =
tr [] (List.filterMap viewHeaderCell headerGroup.headers)
Header list functions
When you want a flat list rather than the rows, these return one:
| TanStack | elm-table |
|---|---|
table.getFlatHeaders() |
flatHeaders |
table.getLeafHeaders() |
leafHeaders |
header.getLeafHeaders() |
getLeafHeaders |
table.getStartFlatHeaders() |
leftFlatHeaders |
table.getCenterFlatHeaders() |
centerFlatHeaders |
table.getEndFlatHeaders() |
rightFlatHeaders |
table.getStartLeafHeaders() |
leftLeafHeaders |
table.getCenterLeafHeaders() |
centerLeafHeaders |
table.getEndLeafHeaders() |
rightLeafHeaders |
flatHeaders is every header of every
header row, group headers and placeholders included.
leafHeaders is only the headers with
no sub-headers, which is one per rendered column.
flatHeaderIds : Table.State -> List String
flatHeaderIds state =
List.map Table.headerId (Table.flatHeaders config state)
leafHeaderIds : Table.State -> List String
leafHeaderIds state =
List.map Table.headerColumnId (Table.leafHeaders config state)
getLeafHeaders is the per-header
version: the columns one header actually covers, useful when a click on a
group header should act on everything below it.
coveredColumns : Table.Header Person -> List String
coveredColumns header =
List.map Table.headerColumnId (Table.getLeafHeaders header)
Header objects
Every reader takes the header and nothing else.
| TanStack | elm-table |
|---|---|
header.id |
headerId |
header.column.id |
headerColumnId |
header.colSpan |
headerColSpan |
header.rowSpan |
headerRowSpan |
header.depth |
headerDepth |
header.index |
headerIndex |
header.isPlaceholder |
headerIsPlaceholder |
header.placeholderId |
headerPlaceholderId |
header.subHeaders |
headerSubHeaders |
header.getSize() |
getHeaderSize |
header.getStart() |
getHeaderStart |
header.column |
none, use headerColumnId then findColumn |
header.headerGroup |
none |
header.getContext() |
none |
Header ids
headerId is unique within the header rows. For a plain header it is the
column id. A group header, a placeholder, or a header in a pinned region gets
a compound id built from the region, the depth, the column id, and the id of
its first child, joined with underscores. Use it as the key of a keyed node,
not as something to parse.
There is no header.column
A header carries the column's id, not the column. To get the column, look it up:
headerColumn : Table.Header Person -> Maybe (Table.Column Person)
headerColumn header =
Table.findColumn config (Table.headerColumnId header)
That is the one extra step compared with TanStack. It comes up whenever you want the header text, since the text lives on the column:
headerLabel : Table.Header Person -> String
headerLabel header =
headerColumn header
|> Maybe.andThen Table.columnHeader
|> Maybe.withDefault (Table.headerColumnId header)
Table.columnHeader returns a Maybe String, because
withHeader is optional. Falling back
to the column id is the usual choice.
There is no header.headerGroup either. You have the header group in scope
when you map over its headers, so pass down what you need from it.
Nested and grouped header properties
These matter once columns are nested with
Table.group:
headerColSpan: how many columns the header spans. Goes straight into thecolspanattribute.headerRowSpan: how many header rows it spans. A leaf column shallower than the deepest leaf column produces a chain of placeholder headers above its real header. The placeholder at the top of the chain reports the whole chain's span, and every header it covers, including the real leaf header at the bottom, reports0.headerDepth: which header row the header is in, counting from0at the top.headerIndex: the header's position from left to right within its header row. Not the same asheaderDepth.headerIsPlaceholder: true for a filler header standing in for a column that has no group at this level.headerPlaceholderId: how many placeholders for the same column came before this one, as a string.Nothingon a real header.headerSubHeaders: the headers nested directly under this one, empty on a leaf header.
childIds : Table.Header Person -> List String
childIds header =
List.map Table.headerId (Table.headerSubHeaders header)
placeholderReport : Table.Header Person -> String
placeholderReport header =
if Table.headerIsPlaceholder header then
Table.headerColumnId header
++ " placeholder "
++ Maybe.withDefault "0" (Table.headerPlaceholderId header)
++ " at depth "
++ String.fromInt (Table.headerDepth header)
++ ", position "
++ String.fromInt (Table.headerIndex header)
else
Table.headerId header
Header row spanning
With an uneven column tree, merge each placeholder chain into one cell:
skip every header whose headerRowSpan is 0, and render the rest with both
attributes. This replaces the usual headerIsPlaceholder check, because the
spanning placeholder is the cell that draws the column's header text.
viewHeaderCell : Table.Header Person -> Maybe (Html msg)
viewHeaderCell header =
if Table.headerRowSpan header == 0 then
Nothing
else
Just
(th
[ colspan (Table.headerColSpan header)
, rowspan (Table.headerRowSpan header)
]
[ text (headerLabel header) ]
)
This recipe is for <thead> only. footerGroups returns the header rows in
reverse, which puts a spanning placeholder below the cells it would need to
cover, so keep the empty-cell placeholder pattern in <tfoot>. See
Header Groups.
The body-cell equivalent is Cell Spanning.
Header rendering
TanStack renders a header with flexRender(header.column.columnDef.header, header.getContext()) because a header column option can be a string, JSX,
or a function. Here
withHeader takes a String and you
write the markup:
viewHeaderCell : Table.State -> Table.Header Person -> Html msg
viewHeaderCell state header =
th
[ colspan (Table.headerColSpan header)
, style "width" (String.fromFloat (Table.getHeaderSize config state header) ++ "px")
]
[ text (headerLabel header) ]
For a sortable header, put the click handler and the direction arrow in that same function. See Sorting.
Sizing
getHeaderSize is a leaf header's
column width, or the sum of the sub-header widths for a parent header.
getHeaderStart is how far from
the start of its header row the header begins, which is what a sticky pinned
header needs. It takes the headers of the row the header belongs to, because
there is no header group reference to walk back to. Pass
headerGroup.headers, not flatHeaders:
viewStickyHeaderRow : Table.State -> Table.HeaderGroup Person -> Html msg
viewStickyHeaderRow state headerGroup =
tr []
(List.map
(\header ->
th
[ style "position" "sticky"
, style "left" (offsetPx state headerGroup.headers header)
]
[ text (headerLabel header) ]
)
headerGroup.headers
)
offsetPx : Table.State -> List (Table.Header Person) -> Table.Header Person -> String
offsetPx state headerRow header =
String.fromFloat (Table.getHeaderStart config state headerRow header) ++ "px"
See Column Sizing and Column Resizing.