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 msgtype 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.