Commands and messages from/to child models

I would start with nested TEA and then look at derivatives from there. This has the benefit that you need to understand TEA anyway to work with Elm and TEA proves relatively easy to make “fractal” provided you only need to have parents route messages to children.

If you need to communicate with a “top-level” set of code such as error reporting or security-token attachment, then look at the service pattern approach. (A service pattern) Here you replace Cmd with an application defined Request type that gets translated into commands at the top level as needed, but conceptually it’s essentially the same as TEA for most development. This is also a way to build things that work like effects managers but do so in “user space”. It also provides a good way to encapsulate ports if they need to be accessed from multiple points in your code. What it doesn’t handle, that native Elm does handle, is subscriptions; you can’t subscribe to a service and have that get cleaned up when you lose interest.

When dealing with child-to-parent communication, we need to look at the cases we are dealing with. In particular, is the child informing the parent of something where the child may not actually have any other interest or does the child need the parent to respond? What sort of time-constraints exist on responses by the parent and in particular does it need to be synchronous or is an asynchronous response acceptable?

What I’ve found useful in thinking about these relationships is that while TEA distills everything down to one model type and one message type, we actually have 4 types that can be used to structure the communication from child to parent.

First, we have the child model that is stored by the parent and that is the target of update messages. The traditional TEA update function returns an updated model as part of its result. At that point, all the parent can do is store this updated model. The parent could look inside the returned model — possibly via an API provided by the child — and decide on further action, but the child has no way to force the parent to notice a change. But note that the returned model type from an update need not be the same as the submitted model type. This gives us a way to force the parent to notice state changes. For example, if the child model represents some modal query and we would like a way to close that part of the user interface when done, we could structure the child’s update function as:

update : Child.Msg -> Child.Model -> ( Maybe Child.Model, Child.Msg )

If the child wishes to continue running, it returns Just newModel and if the child is done, it returns Nothing. This pattern extends readily to handle things like ending a login session by returning a done value that includes login credentials.

The other place where we can readily make extensions is by observing that the message type passed to the child update function need not be the same type returned by the result of the child view function nor of the commands generated as part of update. Instead, we can break things up as:

module Child
type SelfMsg = …
type Msg = ToSelf SelfMsg | …
type Model = …
update : SelfMsg -> Model -> ( Model, Cmd Msg )
view : Model -> Html Msg

The essence here is that the child can generate a range of messages. Some of these messages are meant for the child and get wrapped in ToSelf while others are meant for the parent or whomever the parent chooses to delegate these messages to. This is much like the translator pattern but instead of providing a record of translation functions to the child, the child says essentially “Here are all of the messages I can produce, please figure out what you want to do with them” and parents can do that work via Html.map and Cmd.map — calls which nested TEA would already have forced them to include those often with just the ToChild constructor as the mapping function. The parent would now map using a function like mapChildMsg which would case on the child message wrapping messages bound for the child (ToSelf childMsg) in delivery wrappers (ToChild childMsg) while reacting to other messages themselves or even passing them up another level if necessary.

The net in my experience is that if you understand TEA and you understand nested TEA, then the combination of the service pattern and exploiting some of the flexibilities around types in TEA, you can build well-factored, large Elm applications while having the code keep a rhythm that is close to basic TEA.

6 Likes