Render props for Elmby@julianjelfs_61852
448 reads

Render props for Elm

Read on Terminal Reader

Too Long; Didn't Read

featured image - Render props for Elm
Boolean Julian Jәlfs HackerNoon profile picture


Boolean Julian Jәlfs
react to story with heart

Well, something like that any way.

This is a common UI pattern that is a bit difficult to achieve in Elm. Something like render props from the React world or transclusion if you have escaped from the angular world.

The idea is that you would like to create a generic component to provide perhaps some wrapper UI and a little bit of state management and then re-use that component but inject your own view (render prop) into it. Good examples of when you might reach for this technique would be a modal or a tab strip. In these cases you don’t want to rewrite the markup and logic that are generic each time. A modal has a header and a footer and you need to track whether it is open or closed etc, but mostly what you care about when you use it is the content for your particular use case.

So what’s the problem?

It’s in the types. Let’s try to do it and we will see why it doesn’t immediately work. For the sake of simplicity let’s say that my generic component will basically just be a div and that I would like to be able to render my custom container component but inject custom UI into it. My div will have a header that changes colour when you click it (that’s the state that it tracks internally that I want to abstract away).

Let’s try it

Our ColourClicker component will have the usual Elm lifecycle since it needs to track this little bit of internal state and it might look something like this initially:

type alias Model ={ colour : String }

type Msg= ToggleColour

init : Modelinit ={ colour = "blue" }

update : Msg -> Model -> Modelupdate msg model =case msg ofToggleColour ->toggleColour model

toggleColour : Model -> ModeltoggleColour model =case model.colour of"blue" ->{ model | colour = "red" }_ ->{ model | colour = "blue" }

view : Model -> List (Html Msg) -> Html Msgview model content =div[ class "colour-clicker" ][ h1[ onClick ToggleColour, style "background" model.colour][ text "This is the magical header" ], div []content]

Hopefully this doesn’t need too much explanation. The view function simply allows us to inject content from the consuming code.

The code that uses this component might look a bit like this:

import ColourClicker as C

type alias Model ={ colourClicker : C.Model }

init : ( Model, Cmd Msg )init =( { colourClicker = C.init }, Cmd.none )

type Msg= ColourClickerMsg C.Msg

update : Msg -> Model -> ( Model, Cmd Msg )update msg model =case msg ofColourClickerMsg subMsg ->letsubModel =C.update subMsg model.colourClickerin( { model | colourClicker = subModel }, Cmd.none )

view : Model -> Html Msgview model =div [ class "demo" ][ h1 [] [ text "Demo of render props technique for Elm" ], ColourClickerMsg <|C.view model.colourClicker[ div [][ text "This is the content of the colour clicker provided by the parent" ]]]

Then end result looks like this and works fine:

New feature request

Now suppose that I want the content that I inject to contain a button and I want to handle the clicking of that button in my parent component. Like this:

type Msg= ColourClickerMsg C.Msg| ButtonClicked

update : Msg -> Model -> ( Model, Cmd Msg )update msg model =case msg of...

    ButtonClicked ->  
            \_ =  
                Debug.log "Button Clicked!" ()  
        ( model, Cmd.none )

view : Model -> Html Msgview model =div [ class "demo" ][ h1 [] [ text "Demo of render props technique for Elm" ], ColourClickerMsg <|C.view model.colourClicker[ div [][ text "Now we need to add a button", button [ onClick ButtonClicked ] [ text "Click me!" ]]]]

This is a reasonable thing to want to do. We don’t want the ColourClicker component itself to handle this button click because the logic belongs in the calling component. The ColourClicker should only be responsible for its own state (which is the colour of its header). But we have a problem. Our ColourClicker’s view function expects its content to be of type Html ColourClicker.Msg but we are now trying to pass in Html Parent.Msg so the types do not line up and the compiler (rightly) complains.


So what can we do?

Let’s follow the types. Firstly, we must make the ColourClicker’s view function accept content with a polymorphic type like this:

view : Model -> List (Html parent) -> Html Msg

In this signature “parent” is a type parameter to indicate that we do not know what type this Html will have. This gets rid of the previous compiler error but replaces it with a new one.


The issue now is that our ColourPicker’s view is trying to embed Html parent within a block of Html ColourPicker.Msg which is not allowed. So it is clear that we need to somehow map the Html that has been passed in to coerce it to the right type.

To do this we need to create a new Msg type within the ColourClicker to which to map the parent Msg type, create a function to perform the mapping and call that function when we write out the content:

type Msg parent= ToggleColour| Parent parent

wrap : Html parent -> Html (Msg parent)wrap Parent

view : Model -> List (Html parent) -> Html (Msg parent)view model content =div[ class "colour-clicker" ][ h1[ onClick ToggleColour, style "background" model.colour][ text "This is the magical header" ], div []( wrap content)]

What we have done is to parameterise our own Msg type with the polymorphic parent type such that we can create an instance of our Msg type that contains the parent msg. Note that all of this is transparent to the calling code and introduces no further complexity. This makes the types line up, but isn’t it just a trick? What happens to our update function? It needs to change as follows:

update : Msg parent -> Model -> ( Model, Cmd parent )update msg model =case msg ofToggleColour ->( toggleColour model, Cmd.none )

    Parent p ->  
        ( model, Task.perform identity (Task.succeed p) )

First we need to change the return type to allow us to return a Cmd parent. This is unusual — ordinarily it would be Cmd Msg. We could do that as well if we needed to, but in this case we don’t. Remember there is nothing magical about the update function, it’s just a function — we can do what we like. In the branch of the case statement that handles the new Parent parent message type we simply convert that message into a Cmd using the Task api and return it to the caller.

The final change required then is to the parent’s update function to handle this new return type correctly. This now looks like this:

case msg ofColourClickerMsg subMsg ->let( subModel, subCmd ) =C.update subMsg model.colourClickerin( { model | colourClicker = subModel }, subCmd )

The subCmd that comes out of the update function is already the right type to just return directly from the parent’s update function. It will ultimately get fed back into the update function as the ButtonClicked msg.

Everything now compiles! We have really nice idiomatic syntax and we are able to naturally handle the button click at the parent level without the ColourClicker component ever knowing anything about it. We can now reuse the ColourClicker component much more effectively.

I’d love to hear other approaches to this problem because it took me a while to figure this out and it seems like such a common requirement.

The code for this example can be found here. Hope this is useful for someone.


. . . comments & more!
Hackernoon hq - po box 2206, edwards, colorado 81632, usa