Grouping
Group rows by one or more columns. Ports examples/react/grouping from TanStack Table.
Grouping.elm
module Grouping exposing (main)
{-| Ports `examples/react/grouping/src/main.tsx`.
Click 👊 in a header to group by that column; the grouped columns move to the
front (`groupedColumnMode: 'reorder'`, the default) and each group row gets an
expander with its child count. `firstName` overrides the value it groups by
with `Table.withGetGroupingValue`, so two people with the same first name but
different last names land in different groups.
-}
import Browser
import Html exposing (Html, button, div, table, tbody, td, text, th, thead, tr)
import Html.Attributes exposing (class, classList, colspan)
import Html.Events exposing (onClick)
import Shared.Controls as Controls
import Shared.People as People exposing (Person)
import Table
import Table.Value as Value exposing (Value)
-- CONFIG
config : Table.Config Person
config =
Table.config
[ Table.column "firstName" (.firstName >> Value.String)
|> Table.withHeader "First Name"
-- override the value used for row grouping (otherwise, defaults to
-- the value derived from the accessor)
|> Table.withGetGroupingValue
(\person _ ->
Value.String
(person.firstName ++ " " ++ Maybe.withDefault "" person.lastName)
)
, Table.column "lastName" (.lastName >> maybeString)
|> Table.withHeader "Last Name"
, Table.column "age" (.age >> toFloat >> Value.Number)
|> Table.withHeader "Age"
, Table.column "visits" (.visits >> maybeNumber)
|> Table.withHeader "Visits"
, Table.column "status" (.status >> People.statusToString >> Value.String)
|> Table.withHeader "Status"
, Table.column "progress" (.progress >> toFloat >> Value.Number)
|> Table.withHeader "Profile Progress"
]
|> Table.withGetRowId (\person _ _ -> person.id)
maybeString : Maybe String -> Value
maybeString =
Maybe.map Value.String >> Maybe.withDefault Value.Null
maybeNumber : Maybe Int -> Value
maybeNumber =
Maybe.map (toFloat >> Value.Number) >> Maybe.withDefault Value.Null
-- MODEL
rowCount : Int
rowCount =
2000
type alias Model =
{ state : Table.State
, seed : Int
, data : List Person
}
init : Model
init =
{ state = Table.initialState
, seed = 42
, data = People.makeData 42 [ rowCount ]
}
type alias Stages =
{ core : Table.RowModel Person
, prePaginated : Table.RowModel Person
, paginated : Table.RowModel Person
}
stages : Model -> Stages
stages model =
let
core : Table.RowModel Person
core =
Table.coreRowModelFromList config model.state model.data
prePaginated : Table.RowModel Person
prePaginated =
core
|> Table.filteredRowModel config model.state
|> Table.groupedRowModel config model.state
|> Table.sortedRowModel config model.state
|> Table.expandedRowModel config model.state
in
{ core = core
, prePaginated = prePaginated
, paginated = Table.paginatedRowModel config model.state prePaginated
}
-- UPDATE
type Msg
= RegenerateData
| GroupingToggled String
| ExpandToggled (Table.Row Person)
| FirstPage
| PreviousPage
| NextPage
| LastPage
| GoToPage String
| SetPageSize String
update : Msg -> Model -> Model
update msg model =
case msg of
RegenerateData ->
let
seed : Int
seed =
model.seed + 1
in
{ model | seed = seed, data = People.makeData seed [ rowCount ] }
GroupingToggled columnId ->
{ model | state = Table.toggleGrouping columnId model.state }
ExpandToggled row ->
{ model
| state =
Table.toggleExpanded config (stages model).prePaginated row Nothing model.state
}
FirstPage ->
{ model | state = Table.firstPage config model.state }
PreviousPage ->
{ model | state = Table.previousPage config model.state }
NextPage ->
{ model | state = Table.nextPage config model.state }
LastPage ->
{ model | state = Table.lastPage config model.state (stages model).prePaginated }
GoToPage typed ->
{ model
| state =
Table.setPage config
(Maybe.withDefault 1 (String.toInt typed) - 1)
model.state
}
SetPageSize typed ->
{ model
| state = Table.setPageSize (Maybe.withDefault 10 (String.toInt typed)) model.state
}
-- VIEW
view : Model -> Html Msg
view model =
let
current : Stages
current =
stages model
state : Table.State
state =
model.state
in
div [ class "demo-root" ]
[ div [ class "button-row" ]
[ button [ onClick RegenerateData ] [ text "Regenerate Data" ] ]
, div [ class "spacer-sm" ] []
, table []
[ thead []
[ tr [] (List.map (viewHeaderCell state) (Table.flatHeaders config state)) ]
, tbody [] (List.map (viewRow model) current.paginated.rows)
]
, div [ class "spacer-sm" ] []
, Controls.pager
{ first = FirstPage
, previous = PreviousPage
, next = NextPage
, last = LastPage
, goToPage = GoToPage
, setPageSize = SetPageSize
, canPrevious = Table.getCanPreviousPage state
, canNext = Table.getCanNextPage config state current.prePaginated
, canLast = Table.getCanLastPage config state current.prePaginated
, pageIndex = state.pagination.pageIndex
, pageCount = Table.getPageCount config state current.prePaginated
, pageSize = state.pagination.pageSize
, pageSizes = [ 10, 20, 30, 40, 50 ]
, showAll = False
}
, div [] [ text (Controls.formatInt (List.length current.paginated.rows) ++ " Rows") ]
, Controls.stateDump state
]
viewHeaderCell : Table.State -> Table.Header Person -> Html Msg
viewHeaderCell state header =
let
columnId : String
columnId =
Table.headerColumnId header
in
th [ colspan (Table.headerColSpan header) ]
[ div []
[ if Table.getCanGroup config columnId then
button [ class "small", onClick (GroupingToggled columnId) ]
[ text
(if Table.getIsGrouped state columnId then
"🛑(" ++ String.fromInt (Table.getGroupedIndex state columnId) ++ ") "
else
"👊 "
)
]
else
text ""
, text
(" "
++ (Table.findColumn config columnId
|> Maybe.andThen Table.columnHeader
|> Maybe.withDefault columnId
)
)
]
]
viewRow : Model -> Table.Row Person -> Html Msg
viewRow model row =
tr []
(List.map (viewCell model row) (Table.getAllCells config model.state row))
viewCell : Model -> Table.Row Person -> Table.Cell -> Html Msg
viewCell model row cell =
let
state : Table.State
state =
model.state
isGrouped : Bool
isGrouped =
Table.cellIsGrouped state row cell.columnId
isPlaceholder : Bool
isPlaceholder =
Table.cellIsPlaceholder state row cell.columnId
in
td
[ classList
[ ( "grouped", isGrouped )
, ( "placeholder", isPlaceholder )
]
]
[ if isGrouped then
-- If it's a grouped cell, add an expander and row count
button [ class "sortable", onClick (ExpandToggled row) ]
[ text
((if Table.getIsExpanded config state row then
"👇 "
else
"👉 "
)
++ cellText cell
++ " ("
++ Controls.formatInt (List.length (Table.rowSubRows row))
++ ")"
)
]
else if isPlaceholder || Table.rowIsGrouped row then
-- For cells with repeated values, render nothing
text ""
else
text (cellText cell)
]
cellText : Table.Cell -> String
cellText cell =
if cell.columnId == "progress" then
Controls.round2 (Value.toNumber cell.value) ++ "%"
else
Controls.valueToString cell.value
main : Program () Model Msg
main =
Browser.sandbox { init = init, update = update, view = view }