Column Definitions

Column definitions are the largest thing you write. They decide:

  • what value each column reads out of a row, which is what sorting, filtering, grouping, faceting, and aggregation all operate on;
  • the header rows, including nested header groups, and the footers;
  • which columns exist for display only, such as a checkbox or a row-actions button.

This page is the Elm counterpart of TanStack Table's Column Definitions Guide. It is about building Table.Column values with Table.column, Table.display, and Table.group. For reading a column back during rendering, see Columns.

Column Def Types

The same three kinds of column exist here, and each has its own constructor.

Kind Constructor Has an accessor What it is for
Accessor column Table.column id accessor Yes A column with an underlying value. Can be sorted, filtered, grouped, aggregated, and faceted.
Display column Table.display id No Arbitrary content: a selection checkbox, an expander, a row-actions button.
Group column Table.group id children No Nesting. Produces a header spanning its children and no cells of its own.

Display and group columns have no value, so the sorting, filtering, and grouping options below have nothing to work with on them. Setting one is not an error; it simply never applies.

selectColumn : Table.Column Person
selectColumn =
    Table.display "select"
        |> Table.withSize 40
        |> Table.withEnableHiding False
groupedColumns : List (Table.Column Person)
groupedColumns =
    [ Table.group "name"
        [ Table.column "firstName" (.firstName >> Value.String)
            |> Table.withHeader "First"
        , Table.column "lastName" (.lastName >> Value.String)
            |> Table.withHeader "Last"
        ]
        |> Table.withHeader "Name"
    , Table.group "employment"
        [ Table.column "department" (.department >> Value.String)
            |> Table.withHeader "Department"
        , Table.column "salary" (.salary >> Value.Number)
            |> Table.withHeader "Salary"
        ]
        |> Table.withHeader "Employment"
    ]

Table.group stamps each child with its parent id and its depth, so a column tree needs no table instance to make sense of. Header Groups covers the header rows that come out of it.

Column Helpers

TanStack's createColumnHelper exists so TypeScript can infer the value type of a column from the key or function you gave it, and so the feature set is carried in the type. Neither problem exists here.

Table.column takes the accessor directly, and Elm infers row from it. A list of Table.Column Person is checked against your data when you pass it to Table.rowsFromList, so a column that reads the wrong record is a compile error at the point you wrote it. There is no helper to construct and no type parameter to pass.

Creating Accessor Columns

An accessor column is an id and a function from a row to a Value:

Table.column : String -> (row -> Value) -> Column row

Value is the one union type the built-in sort, filter, and aggregation functions work on. Its constructors are String, Number, Bool, Date, List, and Null.

accessorColumns : List (Table.Column Person)
accessorColumns =
    [ Table.column "firstName" (.firstName >> Value.String)
        |> Table.withHeader "First name"
    , Table.column "age" (.age >> toFloat >> Value.Number)
        |> Table.withHeader "Age"
    , Table.column "active" (.active >> Value.Bool)
        |> Table.withHeader "Active"
    , Table.column "tags" (.tags >> List.map Value.String >> Value.List)
        |> Table.withHeader "Tags"
    ]

Note the shapes: Value.Number takes a Float, so an Int field goes through toFloat; Value.List takes a List Value, which is what the array filter functions and the unique and extent aggregations read.

Accessor Functions

Because the accessor is a plain function, a computed column is no different from a stored one. Whatever the function returns is the value the whole pipeline sees:

fullNameColumn : Table.Column Person
fullNameColumn =
    Table.column "fullName"
        (\person -> Value.String (person.firstName ++ " " ++ person.lastName))
        |> Table.withHeader "Name"

Reaching into nested records is the same idea: .name >> .first >> Value.String. See Data.

No accessorKey

TanStack offers a string shorthand, accessorKey: 'firstName', plus a dotted form for nested objects and a numeric-string form for arrays. All three are missing here for one reason: an Elm record cannot be indexed by a string, so there is nothing for a key to look up. Every accessor column takes a function.

Nothing is lost. .firstName >> Value.String is the shorthand's equivalent and is checked by the compiler, 'name.first' becomes .name >> .first, and an array index becomes a pattern match or a List accessor of your own. The warning in TanStack's guide about a literal period inside a key does not apply, because nothing parses the id.

Unique Column IDs

Every column carries the id you gave its constructor. There is no derivation step: no id is guessed from a key and none is taken from a header string, so the two rules TanStack has for when each of those applies collapse into one.

Ids must be unique across the whole column tree, group columns included, because everything else refers to a column by id. Table.getValue, Table.toggleSort, Table.setColumnFilter, State.columnOrder, State.columnVisibility, and State.columnSizing are all keyed by it. A duplicate id is not rejected; it makes the later column unreachable through the earlier one's id.

Table.columnId reads it back, and Table.findColumn looks a column up in a Config by id.

Dynamic Column Definitions

A column list is an ordinary List, so building it from the data is just List.map. See Data for the row type to use when the shape is not known ahead of time, and pick a sort and filter function per column from whatever runtime check you can make on a sample value.

There is no stable-identity rule to observe when you do this. Nothing is memoized against the column list, so rebuilding it is only a question of the work it costs. Build the Config once where it is cheap to, which for a generated column list means when the data arrives, not in view.

Column Formatting and Rendering

This is the largest difference from TanStack. There, a column def carries cell, aggregatedCell, header, and footer render functions, and flexRender dispatches on whatever each one returned.

Here a column carries no render functions at all. You write the Html yourself, in your own view, from the values the package hands you. That is what makes flexRender unnecessary, and it is why the header and footer options are plain String rather than functions.

Cell Formatting

Read the value and render it however you like:

td [] [ text (Value.toString (Table.getValue config row (Table.columnId col))) ]

Value.toString is TanStack's String(value) coercion, useful as a default. Pattern match on the Value instead when you want your own formatting, and reach the original record with Table.rowOriginal when the cell needs more than one field. Cells covers the cell-level queries.

Aggregated Cell Formatting

Group rows read differently from leaf rows, and you branch on that yourself. Table.rowIsGrouped says whether a row is a group row, Table.cellIsGrouped says whether this is the cell of the column being grouped by, and Table.cellIsAggregated says whether the cell holds an aggregated value. Grouping and Aggregation walk through the branches.

Table.withHeader and Table.withFooter take a String, not a render function. Anything richer than text is something your view builds; the string is only the label the column carries.

Both are optional, so Table.columnHeader and Table.columnFooter return Maybe String. Views usually fall back to the id:

headerText : Table.Column Person -> String
headerText col =
    Maybe.withDefault (Table.columnId col) (Table.columnHeader col)

Writing that line once and reusing it is worth doing, because every header cell needs it. Headers covers the header cells themselves, which are what you render for a table with column groups.

Feature Options on Column Defs

Every other option is a with* builder that takes a Column row and returns one, so they pipe:

salaryColumn : Table.Column Person
salaryColumn =
    Table.column "salary" (.salary >> Value.Number)
        |> Table.withHeader "Salary"
        |> Table.withFooter "Total"
        |> Table.withSortFn SortFn.basic
        |> Table.withSortDescFirst True
        |> Table.withFilterFn FilterFn.inNumberRange
        |> Table.withAggregationFn AggregationFn.sum

The tables below list every builder that applies to a column, the field it sets on the internal Column record, and the value that field starts with. They are grouped the way TanStack groups its column options.

Identity and Labels

Builder Field Type Default
Table.column, Table.display, Table.group id String The first argument. Required.
Table.column accessorFn Maybe (row -> Value) Nothing for display and group.
Table.group columns List (Column row) [] for column and display.
Table.withHeader header Maybe String Nothing
Table.withFooter footer Maybe String Nothing

depth and parentId are stamped by Table.group and are not settable; Table.columnDepth and Table.columnParentId read them.

Sorting

Builder Field Type Default Description
Table.withSortFn sortFn Maybe SortFn Nothing The built-in sort function. Nothing means auto-detect from the data, TanStack's 'auto'.
Table.withCustomSort customSort Maybe (Row row -> Row row -> Order) Nothing Compare whole rows, so the comparison can read other columns. Takes precedence over sortFn.
Table.withSortDescFirst sortDescFirst Maybe Bool Nothing Whether the first click sorts descending. Nothing falls back to Config.sortDescFirst, then to the data.
Table.withInvertSorting invertSorting Bool False Flip this column's direction, for values where bigger means worse.
Table.withSortUndefined sortUndefined Maybe SortUndefined Nothing Where Null values land. Nothing behaves as Table.sortNullsLast.
Table.withEnableSorting enableSorting Bool True Allow sorting on this column.
Table.withEnableMultiSort enableMultiSort Maybe Bool Nothing Allow this column in a multi-column sort. Nothing falls back to Config.enableMultiSort.

See Sorting.

Filtering

Builder Field Type Default Description
Table.withFilterFn filterFn Maybe FilterFn Nothing The built-in filter function. Nothing means auto-detect, TanStack's 'auto'.
Table.withCustomFilter customFilter Maybe (Row row -> Value -> Bool) Nothing Decide per row, given the row and the filter value. Takes precedence over filterFn.
Table.withEnableColumnFilter enableColumnFilter Bool True Allow a column filter on this column.
Table.withEnableGlobalFilter enableGlobalFilter Maybe Bool Nothing Include this column in the global filter. Just False opts out; Just True opts in past the default check; Nothing leaves it to Config.getColumnCanGlobalFilter, whose default keeps a column when its first non-null value is a string or a number. A column with no accessor is never included.

See Column Filtering and Global Filtering.

Grouping and Aggregation

Builder Field Type Default Description
Table.withAggregationFn aggregationFn Maybe AggregationFn Nothing How a group row summarises this column. Nothing means auto-detect: sum for a numeric column, extent for a date column, and no aggregation for anything else.
Table.withMaxAggregationDepth maxAggregationDepth Int 0 How far below a group row the aggregation looks. 0 aggregates the group's member rows; 1 descends one level into their children first.
Table.withGetGroupingValue getGroupingValue Maybe (row -> Int -> Value) Nothing The value to group by when it differs from the accessor. The Int is the row's index.
Table.withEnableGrouping enableGrouping Bool True Allow grouping by this column.

See Grouping and Aggregation.

Faceting

Builder Field Type Default Description
Table.withGetUniqueValues getUniqueValues Maybe (row -> List Value) Nothing The faceting values of one row, when a single cell holds several. Nothing uses the accessor's value as the only one.

See Faceting.

Visibility, Pinning, and Sizing

Builder Field Type Default Description
Table.withEnableHiding enableHiding Bool True Allow this column to be hidden.
Table.withEnablePinning enablePinning Bool True Allow this column to be pinned left or right.
Table.withSize size Maybe Float Nothing Width in pixels. Nothing falls back to Config.defaultColumn.size, which is 150.
Table.withMinSize minSize Maybe Float Nothing Minimum width. Falls back to Config.defaultColumn.minSize, which is 20.
Table.withMaxSize maxSize Maybe Float Nothing Maximum width. Falls back to Config.defaultColumn.maxSize, which is 9007199254740991.

Table.columnSize returns the size clamped between the minimum and the maximum. See Column Visibility, Column Pinning, and Column Sizing.

Cell Spanning and Cell Selection

Builder Field Type Default Description
Table.withEnableCellSpanning enableCellSpanning Bool True Allow this column to span.
Table.withSpanRows spanRows Maybe (SpanRows row) Nothing Merge adjacent rows whose value for this column is equal. Null never merges.
Table.withSpanRowsWhen spanRows Maybe (SpanRows row) Nothing The predicate form of the same field, given a RowSpanContext.
Table.withSpanColumns spanColumns Maybe (Row row -> Int) Nothing How many columns this cell spans in a given row. Table.spanAllColumns reaches the end of the region.
Table.withEnableCellSelection enableCellSelection Bool True Allow this column's cells to be selected.

See Cell Spanning and Cell Selection.

Options that live on the Config rather than on a column, including every manual* flag and the table-wide enable flags these columns fall back to, are listed in Config and State.

Column Meta

TanStack column defs take a meta field for hanging your own typed data on a column, usually so a render function can read it back off the column object. There is no counterpart here.

A render function that needs extra information is a function you wrote, so pass it what it needs. When the information really is per column, keep a Dict String yourThing in your own model keyed by column id, which is the same lookup with a type you control.