The Translator Pattern: a model for Child-to-Parent Communication in Elm

Last week, Folkert de Vries wrote an excellent blog post about parent-child communication in Elm. He describes the problem as follows:

When creating more complicated applications, it becomes sensible to break up functionality into components. There is one main model (the parent) that
holds subcomponents (here called children).

A problem arises when the parent needs to respond to something that happens in a child component. For instance, the child validates its input and the parent component needs to respond to that, or the child wants to cause an url change (an effect that affects the global state, and therefore is best done centrally by the parent).

It is not obvious how to do this sort of “upward” communication in Elm. In a typical Elm app, all messages generated by the child are “tagged” by the parent when they’re generated, and forwarded down to the child when they’re received. Ideally, we would like for only some of the messages the child generates to be tagged and passed down; other messages, we’d like to handle directly in the parent.

Folkert considers a simple example: a parent component that needs to show a popup when a certain action occurs in the child. What we’d like is for most messages generated by the Child to be opaque to the parent, passed down and handled by the Child, but for the ShowPopup message to be routed to the parent for direct handling.

Folkert proposes an interesting solution: the child’s update function still responds to all messages generated by the child, but can optionally return an OutMsg to the parent. The parent can then pattern-match on that OutMsg and respond to it accordingly.

This is really nice, but still leaves something to be desired. It requires the child to handle messages it might not need to see, just so it can pass them on to the parent; the parent’s type Msg declaration does not indicate that it might respond to a ShowPopup message; and, as Folkert admits, the pattern can be a little verbose.

Another Solution: The Translator Pattern

Essentially, the parent tells the child how it would like to receive communication. The child returns a custom “translator” to the parent, which translates child messages into parent messages. Then, instead of tagging child-generated messages as they are created, the parent translates them using this translator. The translator acts as a “smart tag” — rather than tag every child-generated message as being just-for-the-child, the translator recognizes that some messages are meant for the parent, and routes them there directly.

Let’s talk about what this looks like for the parent, then we’ll drop down and see how the child implements the pattern.

In Folkert’s toy app, the child component can generate two kinds of messages: internal messages, and a ShowPopup message for the parent. So the parent provides the following translation dictionary, specifying how it would like to translate each of those types of message into parent-component-messages:

-- Assume the parent's message type is:
type Msg
= ChildMsg Child.InternalMsg
| ShowPopup
| OtherParentMsg
-- Then the translation dictionary is:
= { onInternalMessage = ChildMsg
, onShowPopup = ShowPopup

Whereas in a typical Elm app, all messages generated by the child are turned into ChildMsg messages in the parent, here we declare that we want only internal child messages to be tagged; when the child wants us to show a popup, it can just send us a ShowPopup message directly.

If the child component were more complicated, the translation dictionary might have more entries. We’ll consider a Game component that generates three kinds of messages: internal messages just for it, a PlayerLose message when the player loses, and a PlayerWin message that carries an Int with the player’s score. In our parent, we’d like to keep track of a total score across multiple games and change the background color based on how well the player is doing. In this case, we might have the following translation dictionary:

type Msg 
= GameMsg Game.InternalMsg
| IncreaseScore Int
| Penalize
| ResetEverything -- (generated by parent)
= { onInternalMessage = GameMsg
, onPlayerWin = IncreaseScore
, onPlayerLose = Penalize

The parent then uses this translation dictionary to create a Translator. A Translator is just a function from Child.Msg to Parent.Msg. The child exposes a translator function that creates a Translator from a translation dictionary:

gameTranslator = Game.translator translationDictionary

Whenever the parent calls the child’s update or view functions, instead of using ` ChildTag` or ` ChildTag`, it maps the translator — ` gameTranslator` or ` gameTranslator`.

In the parent’s view:

view model = ... gameTranslator (Game.view ...

In the update, we forward the child’s internal messages along as usual, but can deal with translated messages directly:

update msg model =
case msg of
GameMsg internalMsg ->
(game', cmd)
= Game.update internalMsg
{ model | game = game' } ! [ gameTranslator cmd ]
IncreaseScore amt ->
{ model | score = model.score + amt} ! []

Penalize ->
{ model | score = score - 5 } ! []
ResetEverything ->

Tada! This is exactly what we wanted: internal messages are tagged with GameMsg, then update unwraps them and passes them down to Game.update to deal with. But messages that are meant for us — IncreaseScore and Penalize— come straight to us, just like any other message (ResetEverything) in our app.

In the Child

First, the child component defines two types, InternalMsg and OutMsg, then a Msg type that can hold both of these:

module Game exposing (InternalMsg, Translator, translator, view, update)type InternalMsg = GameEvent1 | GameEvent2 | GameEvent3type OutMsg
= PlayerWin Int
| PlayerLose
type Msg = ForSelf InternalMsg | ForParent OutMsg

Given the OutMsgs we have, we then think about what “events” need translation, and create a TranslationDictionary type:

type alias TranslationDictionary msg =
{ onInternalMessage: InternalMsg -> msg
, onPlayerWin: Int -> msg
, onPlayerLose: msg

Notice the types here. Each “dictionary entry” describes how to convert some event that happens in the child into a message of type msg for the parent. When the child generates an InternalMsg, how do we wrap it in a msg? When the player loses, what parent message (of type msg) do we generate? When the player wins the game with a certain Int score, what parent message do we generate? The translation dictionary answers these questions.

A Translator is just a function that converts child messages into parent messages. The child provides a function, translator, that creates such a function given a translation dictionary.

type alias Translator parentMsg = Msg -> parentMsgtranslator : TranslationDictionary parentMsg -> Translator parentMsg
translator { onInternalMessage, onPlayerWin, onPlayerLose } msg =
case msg of
ForSelf internal ->
onInternalMessage internal
ForParent (PlayerWin score) ->
onPlayerWin score
ForParent PlayerLoss ->

Finally, the child’s view and update functions. A couple things to notice below: first, the input to `update` is just an InternalMsg — you don’t have to include boilerplate for handling the other types of message. Second, both view and update must specify whether the messages they are generating are ForSelf or ForParent.

view : Model -> Html Msg
view model =
... div [ onClick (ForSelf GameEvent1) ] [ text "Battle!" ] ...
... div [ onClick (ForParent PlayerLose) ] [ text "Give up" ] ...
-- Same thing with update:
update : InternalMsg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
GameEvent1 -> ...
GameEvent2 -> ...
GameEvent3 -> ...

And that’s it!


A fully worked demo of the “game component” example can be found here:

And a single-module version that works in, if you want to play around with it:

In this demo, the GifGame component provides a simple game in which the player looks at a random gif and tries to guess what query was sent to Giphy to grab that gif. The player has only 10 guesses; if she runs out, she loses. Otherwise the score is how many guesses remain after getting the right answer. The Main app renders two GifGames and uses different translators for each one, so that it can receive messages differently from each of them. The background is colored based on which of the two players has scored more points.

Edit: I’ve now incorporated a suggestion from Alexander Biggs that makes things much nicer — thank you!

Teaching computer science in Boston.

Teaching computer science in Boston.