Commands and messages from/to child models

So, most examples show how to do simple things with one single set of Model, Message, init, update and view. But what if I want to create a second set of Model, Message, init, update, view and use it from the first (Akin to nesting elements into each other)? Should parent wrap commands in this case and then dispatch messages to child? Browser.sandbox/Browser.document has exactly one instance and it is the only way to communicate with “outer world”?

2 Likes

This has been discussed quite a bit over the years.

If you search ‘Elm OutMsg’ or ‘Elm NoMap’ you should find some decent info. Here’s a blog off medium which might help.

The OutMsg, Translator and NoMap patterns all have their individual benefits and trade-offs. Some months ago I needed a grandchild of Main.elm to send a message up to Main.elm. Using OutMsg or Translator would result in the parent getting involved, and I didn’t see why the parent should be involved in Grandparent-Grandchild communication so I started experimenting using ports for this and so far it’s working really nice - I’d be interested to hear if anyone has any reasoning why this is a bad approach.

Basically, you send a msg through a port which is then returned to Elm immediately so that any modules that have registered themselves to receive particular messages will receive them.

The JS side is a really simple one liner:

app.ports.sendMsg.subscribe(msg => app.ports.receiveMsg.send(msg))

The ports module is as simple as:

port module InternalMsg exposing
    ( InternalMsg(..)
    , receiveMsg
    , send
    , subscriptions
    )

import Json.Encode as JE exposing (Value)



{- Types -}


type InternalMsg
    = UnknownMsg
    | SimpleMessage
    | MessageWithData Value


send : InternalMsg -> Cmd msg
send msg =
    sendMsg <|
        case msg of
            MessageWithData payload ->
                { msg = toString (MessageWithData payload)
                , data = payload
                }

            SimpleMessage ->
                { msg = toString SimpleMessage
                , data = JE.null
                }

            UnknownMsg ->
                { msg = toString UnknownMsg
                , data = JE.null
                }



{- Ports -}


port sendMsg : { msg : String, data : Value } -> Cmd msg


port receiveMsg : ({ msg : String, data : Value } -> msg) -> Sub msg



{- Subscriptions -}


subscriptions : ({ msg : InternalMsg, data : Value } -> msg) -> Sub msg
subscriptions =
    map >> receiveMsg



{- Transform -}


map : ({ msg : InternalMsg, data : Value } -> msg) -> { msg : String, data : Value } -> msg
map toMsg { msg, data } =
    case msg of
        "MessageWithData" ->
            toMsg { msg = SidebarLeftStateChange data, data = data }

        "SimpleMessage" ->
            toMsg { msg = Log data, data = data }

        _ ->
            toMsg { msg = UnknownMsg, data = data }


toString : InternalMsg -> String
toString msg =
    case msg of
        UnknownMsg ->
            "Unknown"

        SimpleMessage ->
            "SimpleMessage"

        MessageWithData _ ->
            "MessageWithData"

To send a message:

InternalMsg.send SimpleMessage

To receive:

import InternalMsg exposing (InternalMsg(..))

type Msg
    = InternalMsg_ { msg : InternalMsg, data : Value }
    | ...


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


subscriptions : Model -> Sub Cmd
subscriptions model =
    InternalMsg.subscriptions InternalMsg_
2 Likes

This way of structuring Elm programs with parent-child communication is also sometimes called ‘nested TEA’, since you are nesting Elm architecture modules inside each other.

It can also be thought of as a way of componentizing an application, by splitting it into re-usable pieces that can be assembled with a little bit of glue code to form an application. It is quite an object-oriented way of thinking, and not necessarily the best approach for Elm applications.

That said, you should experiment with nested TEA and out messages to get a feel for how that pattern turns out - but I would not commit to building a larger application this way without considering other patterns.

Also, watch this for some alternative ways of thinking about how to structure a larger application, it is really worth your time and explains some of the issues in trying to build Elm applications as components by default:

3 Likes

I’ve also used this ports strategy. It worked well for me until I switched my ports to use elm-ts-interop, which while I love it, does make it tricky to do this if you have the same port firing from multiple levels of nesting due to the type constraints that elm-ts-interop imposes. If you’re not using elm-ts-interop, or if you don’t have multiple levels of updates firing out the same port, this won’t be a problem.

In my experience, usually I either need a message to go up just one level or to go up to the very top update function. For example, if I have an update function for a todo list, and also an update function for changing the data for an individual todo item, when I want to delete an individual item, I might trigger a DeleteTodo message from the item’s view, but handle it in the update function for the whole list — just one level above. However, if I have a top-level update function that handles making error reports for anything that might happen anywhere in the app, for example, then I need any error data from anywhere in the app to travel up the entire update chain.

Because it’s usually one or the other of those for me, the pattern I usually use when I need to is to make my update functions all return something that looks like kind of this (with some variation from app to app):

type alias UpdateFunctionOutput model msg instruction =
    { model : model
    , cmd : Cmd msg
    , bubbleUp : BubbleUp instruction
    }

type alias BubbleUp instruction =
    { error : Maybe Error
    , notification : Maybe Notification
    , instruction : Maybe instruction
    }

The BubbleUp type alias is the data that gets passed up the chain. The “instruction” field is for passing something up just one level. It may be different for different update functions based on what instructions might need to be available to be passed up. In the todo list example, the instruction type for deleting an item might look something like this:

type Instruction =
  DeleteTodo TodoId

a todo item’s update function’s type would be this:

update : Msg -> Todo -> UpdateFunctionOutput Todo Msg Instruction

You’d pass that DeleteTodo instruction up from the todo item’s update function, and it would get handled by some code in the todo list’s update function.

The error and notification fields are the things that travel to the very top of the application, so they’re set in stone, not type variables like the instruction field. Any update function can put a Just error or a Just notification in there, and it’ll get passed all the way up and handled at the very top. The intermediate update functions just make sure it keeps traveling up.

Not sure how clear this is without more context, but my wrists are bothering me, so I hope it’s at least somewhat helpful as-is!

3 Likes

Here is an example of Nested TEA Nested TEA - Elm Patterns

1 Like

I would consider this to be an antipattern. Ports should be used as they are intended to be used for communication with the external world, and this one looks like a hack. If a parent is not supposed to be involved in grand-child-parent communication, then this is a strange parent component, why it is a parent at all? This all may just be a sign of some problems with the design in the first place.

1 Like

This is exactly what I needed. Not only TEA, but the whole bunch of patterns. It seems I finally understand what’s going on in Elm. Whenever I define main, I actually define a bunch of pure functions that tell browser how to initialize model, how this model should react to events (Messages), what actions model wants to be done (Commands), how to transform model into HTML (view). And It actually works for the whole application at once, not for distinct components, in transactional fashion.

3 Likes

One drawback of using ports to communicate between parts of an Elm application, is that everything needs to be encoded as Value (unless its a simple type, like Int, String, etc.).

A while back, I experimented with a more native messaging service in Elm 0.18 as an effects manager. Sadly, the typing on Cmd does not allow arbitrary message body types, and you also end up needing to encode everything as Value. I also abandoned this because effects packages cannot be published and shared, so it was really a dead end before it even started:

I now think that the idea of passing messages between parts of an Elm application is mostly an anti-pattern. You create the illusion that your application consists of independant processes, communicating with messages, as per the actor model. In reality an Elm application is single threaded, and the top-level Program has just one synchronous update function, so why force the actor model onto this when it does not naturally fit?

I think the real question to ask, is why are things split down into child ‘components’? The reason to create a component is if something is to be re-used many times, in different situations, but with a common interface. This does happen in applications, for sure, but mostly I think the temptation to componentise is because we try and make things into smaller more easily understood units that are assembled to build a whole application, not for genuine re-use. Also because we were taught to work this way in OO languages.

The approach I take now is to start by creating larger files with more in them and to never try and componentize up-front (see also Evan’s ‘Life of a File’ talk). So if I have a page with a menu, I will not split the menu out as a child component, I will just include its model directly in the Model for the page, and so on - its only going to be used on that one page.

Later on, as the structure of the application takes shape, I will look for types that are for re-usable things and split those out. Or maybe just types that have a lot of code associated with them that forms some coherent part of the application that is worth splitting out into its own module, to encapsulate and document some concept that stands well on its own. Or maybe I just have a lot of view code, and its worth splitting it out into a couple of files by area of relevance, just to have smaller files and make it easier to find stuff.

If 2 parts of an Elm application need to pass messages to each other, you really should consider if they should be separate at all? Maybe just shove their Models together and accept that they naturally want to be that way.

Of course, this is a generalisation, there are cases where you do have genuine ‘component’ re-use and message passing makes sense - but its not usually the first pattern you should reach for in Elm, nor does it tend to make applications less complex.

2 Likes

I have the feeling this is one of those things everyone will initially overengineer as part of the learning process and eventually realise the flatter the structure the easier to understand and maintain the codebase.

I’ve tried a whole lot of crazy stuff like deeply nested pages with prerequesite requestd - taking the “make impossible states impossible” to the fullest… it works but its not that fun to work with.

That being said - nested elm architectures is definitely useful to know and understand and then think really hard if you really need it.

5 Likes

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

This topic was automatically closed 10 days after the last reply. New replies are no longer allowed.