Writing a reusable data table function in Elm

Chautelly
5 min readFeb 17, 2017

--

I have been on an Elm journey these past two month. So far it has been a challenging but also enjoyable experience. Coming from a React/Redux environment, and having to let go of reusable Components and switching to reusable functions took some time. But the neurons are connecting and writing code in Elm is becoming less of a challenge and more exciting every day.

I wrote a reusable component with React in my current project to handle rendering data tables that provide pagination, sorting, filtering, and selecting records. The component is used by about 15 different data sets. Naturally I wanted to see how I could develop this in Elm

The beginnings

I started by defining a function that would take List record and return Html msg where both record and msg refer to a generic type

-- Reusable function that will print the length of a list of recordstable : List a -> Html msg
table records =
Html.div [] [ records |> List.length |> toString |> Html.text ]

But how would the table know — how to iterate the fields of the record, and render column headers, rows and cells?

In React I would pass a list of objects describing each record’s field.

  • It’s name,
  • how to receive it’s value,
  • and optionally how to render its value.

The component would then iterate the array of records — and for each record iterate the array of objects describing how to render a specific field as a table cell.

I made a distinction between how to retrieve a record’s field value since sorting and filtering only need raw values, but rendering the table cells could translate to anything. (Text, check-marks, images, etc.)

For this article I am going to use the following data model:

type alias Fruit =
{ id: Int
, name: String
, colour: String
, available: Bool
}

Imagine a list of Fruit models. What our reuseable table function will need is a list of view functions. It can iterate over this list to render each cell by passing the current row’s record to it.

type alias Cells record msg = List (record -> Html msg)table : Cells record msg -> List record -> Html msg
table cells records =
Html.div [ class "table" ] (List.map (tableRow cells) records)
tableRow : Cells record msg -> record -> Html msg
tableRow cells record =
Html.div [ class "table-row" ] []

I’ve defined the type alias Cells record msg here to refer to a list of functions that take a record and return Html msg. Just to keep the functions type definitions readable.

table will take Cells and List record, render a div and and iterate the list of records with List.map using a partially applied function named tableRows.

Partially applied because tableRow is a function that takes Cells then record and returns Html msg

Calling tableRow cells returns a function that takes record. In Elm you don’t have to pass in all the function’s arguments at once.

List.map expects 2 arguments.

  • A function that takes record
  • List record

Since (tableRow cells) is the first argument passed to `List.map` — and returns a function that takes record, List.map is happy.

Let’s continue developing tableRow and also a function named tableCell

module Table exposing (table, Cell, Row)import Html exposing (Html, div)
import Html.Attributes exposing (class)
type alias Cell record msg =
record -> Html msg
type alias Row record msg =
List (Cell record msg)
table : Row record msg -> List record -> Html msg
table row records =
Html.div [ class "table" ] (List.map (tableRow row) records)
tableRow : Row record msg -> record -> Html msg
tableRow row record =
Html.div [ class "table-row" ] (List.map (tableCell record) row)
tableCell : record -> Cell record msg -> Html msg
tableCell record cell =
Html.div [ class "table-cell" ] [ cell record ]

I have redefined the type alias Cells record msg to Row record msg to be a list of Cell record msg. Cell record msg referring to a function that takes record and returns Html msg.

tableRow uses List.map to iterate over row with a function returned by calling tableCell record

tableCell receives the record and cell view function and returns a div with the result of cell record as it’s child.

I’m not going to discuss styling the table here. I use flexbox to style rows and cells.

Fruits of labour

Our list of fruits

fruits : List Fruit
fruits =
[ Fruit 1 "Apple" "Red" True
, Fruit 2 "Banana" "Yellow" False
, Fruit 3 "Orange" "Orange" True
]

Row and Cells
Since Cell record msg is an alias of (record -> Html msg) and Row record msg an alias of List (Cell record msg) lets create a function for each field in our Fruit Record alias.

For now we’ll also define a message type with not much to it. We will get to that part later.

type Msg = MsgidCell : Fruit -> Html Msg
idCell fruit =
Html.div [] [ text (toString fruit.id) ]
nameCell : Fruit -> Html Msg
nameCell fruit =
Html.div [] [ text fruit.name ]
colourCell : Fruit -> Html Msg
colourCell fruit =
Html.div [] [ text fruit.colour ]
availableCell : Cell Fruit Msg
availableCell fruit =
Html.div []
[ text
(if fruit.available then
"Yes"
else
" No"
)
]

All our cell functions take a Fruit model and return Html Msg. We know about the model type, Fruit, and and message type, Msg.

If we imported the type alias Cell from our table module, the type definition for each cell function could also be defined as: Cell Fruit Msg instead of Fruit -> Html Msg

import Table exposing (table, Cell, Row)...colourCell : Cell Fruit Msg
colourCell fruit =
Html.div [] [ text fruit.colour ]

Since Cell is defined with two generic types, record and msg, here we are defining what those generic types will be.

fruitRow : Row Fruit Msg
fruitRow =
[ idCell
, nameCell
, colourCell
, availableCell
]

Now that we have a data set fruits and a list of Cell Fruit Msg we can try rendering our list:

main : Html Msg
main = table fruitRow fruits

You can run elm-reactor to test your reusable table function.

Summary

table is a function that takes:

  • A list of view functions Cell record msg a.k.a (record -> Html msg)
  • A list of records List record

and renders a table by iterating over each record, and with each record iterates over each cell view function.

table does not know what the record looks like, nor what message type it returns. This makes it reusable for different types of records.

You can find a the source for here: https://github.com/rjdestigter/elm-reusable-datatable

Ellie:

Feedback is most welcome. I am still learning Elm myself and this is also the first article I have ever written.

--

--