xdelph / elm-sortable-table / Table

This library helps you create sortable tables. The crucial feature is that it lets you own your data separately and keep it in whatever format is best for you. This way you are free to change your data without worrying about the table “getting out of sync” with the data. Having a single source of truth is pretty great!

I recommend checking out the examples to get a feel for how it works.

View

view : Config data msg -> State -> List data -> Html msg

Take a list of data and turn it into a table. The Config argument is the configuration for the table. It describes the columns that we want to show. The State argument describes which column we are sorting by at the moment.

Note: The State and List data should live in your Model. The Config for the table belongs in your view code. I very strongly recommend against putting Config in your model. Describe any potential table configurations statically, and look for a different library if you need something crazier than that.

Configuration

config : { toId : data -> String, toMsg : State -> msg, columns : List (Column data msg) } -> Config data msg

Create the Config for your view function. Everything you need to render your columns efficiently and handle selection of columns.

Say we have a List Person that we want to show as a table. The table should have a column for name and age. We would create a Config like this:

import Table

type Msg = NewTableState State | ...

config : Table.Config Person Msg
config =
  Table.config
    { toId = .name
    , toMsg = NewTableState
    , columns =
        [ Table.stringColumn "Name" .name
        , Table.intColumn "Age" .age
        ]
    }

You provide the following information in your table configuration:

See the examples to get a better feel for this!

stringColumn : String -> (data -> String) -> Column data msg

intColumn : String -> (data -> Basics.Int) -> Column data msg

floatColumn : String -> (data -> Basics.Float) -> Column data msg

State


type State

Tracks which column to sort by.

initialSort : String -> State

Create a table state. By providing a column name, you determine which column should be used for sorting by default. So if you want your table of yachts to be sorted by length by default, you might say:

import Table

Table.initialSort "Length"

Sorted Data Access

getSortedData : Config data msg -> State -> List data -> List data

Return the data sorted exactly as it will be displayed on the screen.

Custom header

customThead : String -> List ( String -> msg, String, Html msg ) -> List ( String, Status, Html.Attribute msg ) -> HtmlDetails msg

Crazy Customization

If you are new to this library, you can probably stop reading here. After this point are a bunch of ways to customize your table further. If it does not provide what you need, you may just want to write a custom table yourself. It is not that crazy.

Custom Columns


type Column data msg

Describes how to turn data into a column in your table.

customColumn : { name : String, viewData : data -> String, sorter : Sorter data } -> Column data msg

Perhaps the basic columns are not quite what you want. Maybe you want to display monetary values in thousands of dollars, and floatColumn does not quite cut it. You could define a custom column like this:

import Table

dollarColumn : String -> (data -> Float) -> Column data msg
dollarColumn name toDollars =
    Table.customColumn
        { name = name
        , viewData = \data -> viewDollars (toDollars data)
        , sorter = Table.decreasingBy toDollars
        }

viewDollars : Float -> String
viewDollars dollars =
    "$" ++ String.fromInt (round (dollars / 1000)) ++ "k"

The viewData field means we will displays the number 12345.67 as $12k.

The sorter field specifies how the column can be sorted. In dollarColumn we are saying that it can only be shown from highest-to-lowest monetary value. More about sorters soon!

veryCustomColumn : { name : String, viewData : data -> HtmlDetails msg, sorter : Sorter data } -> Column data msg

It is possible that you want something crazier than customColumn. In that unlikely scenario, this function lets you have full control over the attributes and children of each <td> cell in this column.

So maybe you want to a dollars column, and the dollar signs should be green.

import Html exposing (Attribute, Html, span, text)
import Html.Attributes exposing (style)
import Table

dollarColumn : String -> (data -> Float) -> Column data msg
dollarColumn name toDollars =
    Table.veryCustomColumn
        { name = name
        , viewData = \data -> viewDollars (toDollars data)
        , sorter = Table.decreasingBy toDollars
        }

viewDollars : Float -> Table.HtmlDetails msg
viewDollars dollars =
    Table.HtmlDetails []
        [ span [ style "color" "green" ] [ text "$" ]
        , text (String.fromInt (round (dollars / 1000)) ++ "k")
        ]


type Sorter data

Specifies a particular way of sorting data.

unsortable : Sorter data

A sorter for columns that are unsortable. Maybe you have a column in your table for delete buttons that delete the row. It would not make any sense to sort based on that column.

increasingBy : (data -> comparable) -> Sorter data

Create a sorter that can only display the data in increasing order. If we want a table of people, sorted alphabetically by name, we would say this:

sorter : Sorter { a | name : comparable }
sorter =
    increasingBy .name

decreasingBy : (data -> comparable) -> Sorter data

Create a sorter that can only display the data in decreasing order. If we want a table of countries, sorted by population from highest to lowest, we would say this:

sorter : Sorter { a | population : comparable }
sorter =
    decreasingBy .population

increasingOrDecreasingBy : (data -> comparable) -> Sorter data

Sometimes you want to be able to sort data in increasing or decreasing order. Maybe you have race times for the 100 meter sprint. This function lets sort by best time by default, but also see the other order.

sorter : Sorter { a | time : comparable }
sorter =
    increasingOrDecreasingBy .time

decreasingOrIncreasingBy : (data -> comparable) -> Sorter data

Sometimes you want to be able to sort data in increasing or decreasing order. Maybe you have a bunch of data about orange juice, and you want to know both which has the most sugar, and which has the least sugar. Both interesting! This function lets you see both, starting with decreasing order.

sorter : Sorter { a | sugar : comparable }
sorter =
    decreasingOrIncreasingBy .sugar

Custom Tables


type Config data msg

Configuration for your table, describing your columns.

Note: Your Config should never be held in your model. It should only appear in view code.

customConfig : { toId : data -> String, toMsg : State -> msg, columns : List (Column data msg), customizations : Customizations data msg } -> Config data msg

Just like config but you can specify a bunch of table customizations.


type alias Customizations data msg =
{ tableAttrs : List (Html.Attribute msg)
, caption : Maybe (HtmlDetails msg)
, thead : List ( String
, Status
, Html.Attribute msg ) -> HtmlDetails msg
, tfoot : Maybe (HtmlDetails msg)
, tbodyAttrs : List (Html.Attribute msg)
, rowAttrs : data -> List (Html.Attribute msg) 
}

There are quite a lot of ways to customize the <table> tag. You can add a <caption> which can be styled via CSS. You can do crazy stuff with <thead> to group columns in weird ways. You can have a <tfoot> tag for summaries of various columns. And maybe you want to put attributes on <tbody> or on particular rows in the body. All these customizations are available to you.

Note: The level of craziness possible in <thead> and <tfoot> are so high that I could not see how to provide the full functionality and make it impossible to do bad stuff. So just be aware of that, and share any stories you have. Stories make it possible to design better!


type alias HtmlDetails msg =
{ attributes : List (Html.Attribute msg)
, children : List (Html msg) 
}

Sometimes you must use a <td> tag, but the attributes and children are up to you. This type lets you specify all the details of an HTML node except the tag name.


type Status
    = Unsortable
    | Sortable Basics.Bool
    | Reversible (Maybe Basics.Bool)

The status of a particular column, for use in the thead field of your Customizations.

This information lets you do custom header decorations for each scenario.

defaultCustomizations : Customizations data msg

The customizations used in config by default.