Quick Start
elm-table manages a table's state and row processing — sorting, filtering,
grouping, pagination, selection, and the rest — while you keep full control of
the markup. This page gets you from elm install to a rendering table, then
adds the first feature.
Installation
elm install viewengine/elm-table
Your first table
Six steps. Every block below is one declaration from a module that compiles as written; the whole module is at the bottom of the page.
1. Describe your data. Any record type works. The package never inspects it directly, only through the accessors you give it.
type alias Person =
{ id : String
, firstName : String
, lastName : String
, age : Int
}
2. Define your columns. Table.column takes an id and an accessor
turning a row into a Value. Table.withHeader sets the
header text.
columns : List (Table.Column Person)
columns =
[ Table.column "firstName" (.firstName >> Value.String)
|> Table.withHeader "First name"
, Table.column "lastName" (.lastName >> Value.String)
|> Table.withHeader "Last name"
, Table.column "age" (.age >> toFloat >> Value.Number)
|> Table.withHeader "Age"
]
3. Build the Config. This is TanStack's TableOptions. Build it once,
at the top level of your module, not inside view — it holds no state, so
there is nothing to recompute.
config : Table.Config Person
config =
Table.config columns
|> Table.withGetRowId (\person _ _ -> person.id)
withGetRowId gives each row a stable id. Without it a row's id is its index
in the data, which is fine until the data reorders.
4. Put the State in your model. Table.initialState is every slice at
its default: no sorting, no filters, page 0, everything visible.
init : Model
init =
{ state = Table.initialState }
5. Run the pipeline. Table.rowsFromList takes the config, the state, and
your data, and returns a RowModel — the processed rows plus a lookup by id.
Nothing is memoized, so a real app computes this once in update and stores
it; this page recomputes it so each function stays a one-liner. See
Table State for the storing pattern.
rowModel : Table.State -> Table.RowModel Person
rowModel state =
Table.rowsFromList config state people
6. Render. Table.visibleLeafColumns gives the columns to draw,
Table.rowsInDisplayOrder gives the rows for the current page, and
Table.getValue reads one cell.
view : Model -> Html Msg
view model =
let
current =
rowModel model.state
in
table []
[ thead []
[ tr [] (List.map (viewHeader model.state) (Table.visibleLeafColumns config model.state)) ]
, tbody []
(List.map (viewRow model.state) (Table.rowsInDisplayOrder config model.state current))
]
viewRow : Table.State -> Table.Row Person -> Html Msg
viewRow state row =
tr [] (List.map (viewCell row) (Table.visibleLeafColumns config state))
viewCell : Table.Row Person -> Table.Column Person -> Html Msg
viewCell row column =
td [] [ text (Value.toString (Table.getValue config row (Table.columnId column))) ]
A few things to note:
- There is no table instance and nothing to construct.
config,state, and your data are the three arguments every function wants. Table.rowsInDisplayOrderis the list to render. It isrowModel.rowsplus the expanded descendants that pagination left out whenConfig.paginateExpandedRowsisFalse. Row pinning is separate, and lives intopRows,centerRows, andbottomRows.Table.getValuereturns aValue, not aString.Value.toStringgives you TanStack'sString(value)coercion; pattern match on theValuewhen you want your own formatting.- Nothing here is opt-in. Sorting works because the sorting stage is always in the pipeline, not because you registered a feature.
Add a feature: sorting
Sorting needs one message and one state transition. Table.toggleSort cycles
a column through ascending, descending, and unsorted, exactly as clicking a
TanStack header does.
update : Msg -> Model -> Model
update msg model =
case msg of
SortBy columnId ->
{ model
| state =
Table.toggleSort config
(rowModel model.state)
columnId
{ desc = Nothing, multi = False }
model.state
}
The { desc, multi } record is TanStack's toggleSorting(desc, isMulti).
Pass desc = Just True to force a direction; pass multi = True (from a
shift-click) to add the column to the sort instead of replacing it.
toggleSort takes a RowModel because the first direction a column sorts in
can depend on the data: an unsorted column of dates starts descending, a
column of strings starts ascending, the same rule TanStack uses.
The header then reads the direction back out with Table.getIsSorted, which
returns Maybe SortDir:
viewHeader : Table.State -> Table.Column Person -> Html Msg
viewHeader state column =
let
id =
Table.columnId column
arrow =
case Table.getIsSorted state id of
Nothing ->
""
Just dir ->
if dir == Table.sortAsc then
" ↑"
else
" ↓"
in
th [ onClick (SortBy id), style "cursor" "pointer" ]
[ text (Maybe.withDefault id (Table.columnHeader column) ++ arrow) ]
Every other feature follows this pattern: a transition function that returns a
new State, and query functions that read the state back for rendering. See
the Sorting guide for custom sort functions, multi-sorting,
and per-column options.
Wiring it up
Nothing about the package needs a Browser.element or a subscription, so the
whole thing runs as a sandbox:
main : Program () Model Msg
main =
Browser.sandbox { init = init, update = update, view = view }
The complete file every block above came from is
docs-site/snippets/src/QuickStart.elm in the repository, and a runnable
version of the same table is the
Basic example.
Where to go next
Config and State. Config and State is the page to read next: what goes in each, and why there is no third thing.
Table State. Table State covers owning the State
in your model, when to store the RowModel alongside it, and how to reset a
slice.
Feature guides. Each feature has its own page: Column Filtering, Pagination, Row Selection, Grouping, Column Visibility, and the rest.
Examples. Every TanStack example ported to Elm, runnable, is at elm-table-examples.pages.dev, listed on the Examples page.