An Approach to Nested Reusable View Functions in Elm

Update 15 Nov 2018: After reading a comment from Christopher Duncan-Araúz I have realised that the colorSlider should be changed to accept toMsg of type (Int -> msg) and not (String -> msg), since it should pass to toMsg a color value of type Int, and it should do the conversion from String to Int. Thank you, Christopher!


At my work place I wanted to introduce my colleagues to the Elm programming language, so we had two Elm workshops lead by myself.

After getting the basics, one of the most frequent questions was: “How do we reuse components and work with deep nested views?”

Components vs. View Functions

The term component is a very common among modern JavaScript frameworks and usually used when talking about a self contained visual widget and objects. In functional world there are no objects, only functions — hence the term I use in this article is view functions.

The creator of Elm, Evan Czaplicki tweeted about the distinction: https://twitter.com/czaplic/status/903266544544878592.

The closest we probably can get to self containment is by having a module with its model, view and update.

With that, very often things can be further simplified, avoiding the usage of the model, view and update and we will see that here.

Color Picker as an Example

To answer the question about reusability I suggested an example of a color picker.

A Color Picker we want to build
  • We have a slider for each color
  • We have a color picker that (re)uses it by having 3 sliders for each color channel (R, G, b) and shows a square with a corresponding color
  • We have 2 color pickers in the main view

Thus there is 2-deep or even 3-deep (if you count reusing color picker in the main view) nested view.

The Slider

The slider is pretty straightforward since it’s supported by all major browsers:

<input type="range" name="color-red" min="0" max="255" value="50" />

So in Elm it would look something like this:

import Browser
import Html
import Html.Attributes as Attr
import Html.Events as Evt
type alias Model =
{ redValue : String }
initialModel : Model
initialModel =
{ redValue = "50" }
type Msg
= UpdateColorRed String
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateColorRed newRedValue ->
{ model | redValue = newRedValue }
colorRedSlider : String -> Html.Html Msg
colorRedSlider redValue =
Html.input
[ Attr.type_ "range"
, Attr.name "color-red"
, Attr.min "0"
, Attr.max "255"
, Attr.value redValue
, Evt.onInput UpdateColorRed
]
[]
view : Model -> Html.Html Msg
view model =
Html.div []
[ colorRedSlider model.redValue
, Html.span [] [ Html.text model.redValue ]
]
main : Program () Model Msg
main =
Browser.sandbox
{ init = initialModel
, view = view
, update = update
}
Red color channel slider

https://ellie-app.com/3MNbRYJqbNBa1

There is a small issue with the current slider implementation — the red value is a String and we want it to be an Int between 0 and 255.

We can add a function for converting String value to Int:

toInt : Int -> String -> Int
toInt defaultValue strValue =
strValue
|> String.toInt
|> Maybe.withDefault defaultValue

We need to supply to the function a fallback value (defaultValue) since String.toInt returns Maybe Int and we want to get an Int. In our case we always have a previous color channel value, because we have a color to start with and that will be this fallback.

It would also be nice if the Msg can be declared as UpdateColorRed Int instead of UpdateColorRed String. Function composition can help us with this:

Evt.onInput (UpdateColorRed << toInt redValue)

When the user changes slider value, the String representation of it will be passed to the result of toInt redValue — a function that is waiting to get the last parameter (strValue).

This is Elm’s partial application in action. You can read more about it here: https://elmprogramming.com/function.html#partial-function-application.

The result of all that is an Int and it will be passed to UpdateColorRed constructor of type Msg.

The updated fragments of our code looks like this:

{- ommitted code -}
type alias Model =
{ redValue : Int }
initialModel : Model
initialModel =
{ redValue = 50 }
type Msg
= UpdateColorRed Int
{- ommitted code -}
colorRedSlider : Int -> Html.Html Msg
colorRedSlider redValue =
Html.input
[ Attr.type_ "range"
, Attr.name "color-red"
, Attr.min "0"
, Attr.max "255"
, Attr.value (String.fromInt redValue)
, Evt.onInput (UpdateColorRed << toInt redValue)
]
[]
toInt : Int -> String -> Int
toInt defaultValue strValue =
strValue
|> String.toInt
|> Maybe.withDefault defaultValue
view : Model -> Html.Html Msg
view model =
Html.div []
[ colorRedSlider model.redValue
, Html.span [] [ Html.text (String.fromInt model.redValue) ]
]

https://ellie-app.com/3MNmnfTXsjQa1

But we want to reuse the colorRedSlider view and not to hard code it with settings and a specific Msg. Reading through https://guide.elm-lang.org/architecture/forms.html gives us a hint:

viewInput : String -> String -> String -> (String -> msg) -> Html msg
viewInput t p v toMsg =
input [ type_ t, placeholder p, value v, onInput toMsg ] []

We can pass all the custom settings to our colorRedSlider including the toMsg function for onInput.

The concrete Msg type can be changed to type variable msg. There is more about type variables here: https://guide.elm-lang.org/types/reading_types.html.

We also will rename colorRedSlider to colorSlider, since it is now a generic slider and is not bound to a specific color.

Let’s also render a color label and a color value.

view : Model -> Html.Html Msg
view model =
Html.div []
[ colorSlider
"Red"
model.redValue
UpdateColorRed
]
colorSlider : String -> Int -> (Int -> msg) -> Html.Html msg
colorSlider name colorValue toMsg =
Html.div []
[ Html.p [] [ Html.text name ]
, Html.input
[ Attr.type_ "range"
, Attr.name ("color" ++ name)
, Attr.min "0"
, Attr.max "255"
, Attr.value (String.fromInt colorValue)
, Evt.onInput (toMsg << toInt colorValue)
]
[]
, Html.span [] [ Html.text (String.fromInt colorValue) ]
]
Slider with a color label

https://ellie-app.com/3TgrjNCXKDna1

We now have a generic slider view that we can reuse for our color picker!

Let’s put it to work. We’ll add 2 more sliders for the Green and Blue color channels and a box for showing the resulting color:

type alias Model =
{ redValue : Int
, greenValue : Int
, blueValue : Int
}
initialModel : Model
initialModel =
{ redValue = 50, greenValue = 200, blueValue = 100 }
type Msg
= UpdateColorRed Int
| UpdateColorGreen Int
| UpdateColorBlue Int
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateColorRed newRedValue ->
{ model | redValue = newRedValue }
        UpdateColorGreen newGreenValue ->
{ model | greenValue = newGreenValue }
        UpdateColorBlue newBlueValue ->
{ model | blueValue = newBlueValue }
view : Model -> Html.Html Msg
view model =
Html.div []
[ colorSlider "Red" model.redValue UpdateColorRed
, colorSlider "Green" model.greenValue UpdateColorGreen
, colorSlider "Blue" model.blueValue UpdateColorBlue
, Html.div
[ Attr.style "width" "100px"
, Attr.style "height" "100px"
, Attr.style "background-color"
(toColorCss
model.redValue
model.greenValue
model.blueValue
)
]
[]
]
toColorCss red green blue =
"rgb("
++ String.fromInt red
++ ","
++ String.fromInt green
++ ","
++ String.fromInt blue
++ ")"
Color sliders for each color channel

https://ellie-app.com/3TgwYQ3pkLpa1

We have constructed a color picker from 3 color sliders.

The Color Picker

Next we create a colorPicker view that will reuse colorSlider and render the view we have right now.

view : Model -> Html.Html Msg
view model =
Html.div []
[ colorPicker
model.redValue
model.greenValue
model.blueValue
]
colorPicker : Int -> Int -> Int -> Html.Html Msg
colorPicker redValue greenValue blueValue =
Html.div []
[ colorSlider "Red" redValue UpdateColorRed
, colorSlider "Green" greenValue UpdateColorGreen
, colorSlider "Blue" blueValue UpdateColorBlue
, Html.div
[ Attr.style "width" "100px"
, Attr.style "height" "100px"
, Attr.style "background-color"
(toColorCss
model.redValue
model.greenValue
model.blueValue
)
]
[]
]

https://ellie-app.com/3TgBpdyTPJpa1

Let’s improve a little by making our color picker to take a color as an argument and not 3 color channel values.

Since Elm 0.19 there is no Color (elm-lang/core#Color) module in the core library, but there is one in the Elm repository — the-sett/elm-color. Not to confuse it with avh4/elm-color which is very similar, but it gives us color channels only in Float (0.0-1.0) and we want them in Int (0-255). Let’s add it to our project:

> elm install the-sett/elm-color

As a first step we will replace all the individual color values with Color.

import Color exposing (Color)
type alias Model =
{ color : Color
}
initialModel : Model
initialModel =
{ color = Color.rgb 50 200 100 }
update : Msg -> Model -> Model
update msg model =
let
{ red, green, blue } =
Color.toRgb model.color
in
case msg of
UpdateColorRed newRedValue ->
{ model | color = Color.rgb newRedValue green blue }
        UpdateColorGreen newGreenValue ->
{ model | color = Color.rgb red newGreenValue blue }
        UpdateColorBlue newBlueValue ->
{ model | color = Color.rgb red green newBlueValue }
view : Model -> Html.Html Msg
view model =
Html.div []
[ colorPicker model.color
]
colorPicker : Color -> Html.Html Msg
colorPicker color =
let
{ red, green, blue } =
Color.toRgb color
in
Html.div []
[ colorSlider "Red" red UpdateColorRed
, colorSlider "Green" green UpdateColorGreen
, colorSlider "Blue" blue UpdateColorBlue
, Html.div
[ Attr.style "width" "100px"
, Attr.style "height" "100px"
, Attr.style "background-color" (toColorCss color)
]
[]
]
toColorCss : Color -> String
toColorCss color =
let
{ red, green, blue } =
Color.toRgb color
in
"rgb("
++ String.fromInt red
++ ","
++ String.fromInt green
++ ","
++ String.fromInt blue
++ ")"

https://ellie-app.com/3TgG7CRZ4kCa1

We use pattern matching to extract each color channel from a color record:

{ red, green, blue } = Color.toRgb color

Our colorPicker view is tightly coupled with our module — it uses UpdateColorRed, UpdateColorGreen and UpdateColorBlue messages (hence the return type is alsoHtml.Html Msg). We would like to make it generic.

At this point, when I was going over this with people at my work, we moved the color picker code to its own module, that had the triplet model, view and update.

It worked, but it made us need to:

  • have Html.map ColorChange ColorPicker.ColorChange to map Msg from ColorPicker module to main module’s
  • call ColorPicker.update in main module’s update
  • call ColorPicker.view in main module’s view

I am not going show the code for this step, instead we will skip to simplifying the color picker to be similar to the color slider view. It will take a toMsg function that will construct a message that will accept Color.

Lets update the Msg type and the update function:

type Msg
= UpdateColor Color
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateColor newColor ->
{ model | color = newColor }

They look a lot simpler now.

For the colorPicker we’ll add (Color -> msg) and change Msg to msg:

colorPicker : Color -> (Color -> msg) -> Html.Html msg
colorPicker color toMsg =

And just as we used function composition to turn String to Int before, we will use the same technic in order to convert Int to Color:

{- colorSlider -}
toMsg << toInt colorValue
{- colorPicker -}
toMsg << (\newRed -> Color.rgb newRed green blue)

The slider’s onInput will get a String value, which will be passed to toInt and become an Int, then the Int value will be passed to the inline function and be used in the construction of the new Color value, while the rest of the color channel values will remain unchanged.

For readability we can refactor inline functions to their own:

redToColor : Color -> Int -> Color
redToColor color newRed =
let
{ red, green, blue } =
Color.toRgb color
in
Color.rgb newRed green blue
greenToColor : Color -> Int -> Color
greenToColor color newGreen =
let
{ red, green, blue } =
Color.toRgb color
in
Color.rgb red newGreen blue
blueToColor : Color -> Int -> Color
blueToColor color newBlue =
let
{ red, green, blue } =
Color.toRgb color
in
Color.rgb red green newBlue

and use them like this:

toMsg << redToColor color

Let’s also update the view function:

view : Model -> Html.Html Msg
view model =
Html.div []
[ colorPicker model.color UpdateColor
]

The complete colorPicker:

colorPicker : Color -> (Color -> msg) -> Html.Html msg
colorPicker color toMsg =
let
{ red, green, blue } =
Color.toRgb color
in
Html.div []
[ colorSlider "Red" red (toMsg << redToColour color)
, colorSlider "Green" green (toMsg << greenToColour color)
, colorSlider "Blue" blue (toMsg << blueToColour color)
, Html.div
[ Attr.style "width" "100px"
, Attr.style "height" "100px"
, Attr.style "background-color" (toColorCss color)
]
[]
]

https://ellie-app.com/3TgMGjx5Ddra1

We made colorPicker a generic view function, that can be used anywhere!

Reuse

We can put all that code in ColorPicker.Elm and expose only colorPicker.

module ColorPicker exposing (colorPicker)

We can now easily reuse colorPicker in our module:

module Main exposing (..)
import Browser
import Html
import ColorPicker exposing (colorPicker)
type alias Model =
{ color1 : Color
, color2 : Color
}
initialModel : Model
initialModel =
{ color1 = Color.rgb 50 200 100
, color2 = Color.rgb 255 255 255
}
type Msg
= UpdateColor1 Color
| UpdateColor2 Color
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateColor1 newColor1 ->
{ model | color1 = newColor1 }
        UpdateColor2 newColor2 ->
{ model | color2 = newColor2 }
view : Model -> Html.Html Msg
view model =
Html.div []
[ colorPicker model.color1 UpdateColor1
, Html.hr [] []
, colorPicker model.color2 UpdateColor2
]
Color Picker reused in the main view

https://ellie-app.com/3TgPtZcpyK9a1

We added another colorPicker to our application with only a few changes!

As Joël has pointed out — we are passing the whole Color to our update function, although we might pass it only the relevant color channel. There is a discussion on Elm Discourse on this topic: https://discourse.elm-lang.org/t/message-types-carrying-new-state. I chose to pass the whole Color, since our color picker will be reused and it simper to have it handle the whole thing instead of a specific color channel, but you may prefer the other way.

Summary

We saw that reusing view functions in Elm, regardless of how nested they are, is pretty straightforward. This is possible because everything in Elm is… a function, including Msg.

There are cases where it is appropriate to have full blown module with model, view and update, but often it is often preferable to have composable view functions that can take state and message constructors as its arguments.

I hope this was useful for you.

Thank Yous

Special thanks to Joël Quenneville for helping me with this writing.

Thanks to Pawan Poudel for the amazing https://elmprogramming.com/.

Thanks to Evan Czaplicki for giving us Elm.

And thank you for reading!