Objective: Write an app that uses Model-View-Update but is not a giant Main module.

Caveat: This might not be the best way to structure an Elm app. It’s just what I did.


What is the role of Main? Main, in this architecture, is aggregates submodules. It has no (business) logic of its own. This’ll make more sense below.

The Model

The elm example app in the docs starts with this trivial model:

    type alias Model = Int

We could move this to a counter module and change Main.Model to a record like this:

    type alias Model =
        { counter = Counter.Model

The Msg Type

The model was easy. The update and the view are more complicated. We need a way to map the module’s Msg type into something that Main understands. In the example, Main.Msg is defined as:

    type Msg = Increment | Decrement

Start by moving this to the counter module.

Next, we need to update Main.Msg to wrap this type.

    type Msg = CounterMsg Counter.Msg

Easy enough. So how does this change Main.update?

The update

Instead of processing the update directly, we need to delegate it to the module:

    update msg model =
        case msg of
            CounterMsg counterMsg ->
                    (newCounterModel, counterCmd) = Counter.update counterMsg model.counter
                    ( { model | counter = newCounterModel }
                    , Cmd.map CounterMsg counterCmd


There’s a lot going on here. Let’s break it down.

  1. Use case to pattern match on msg like we normally would.
  2. Pass model.counter and counterMsg to Counter.update. We’re simply unwrapping the wrapped values and passing them to module’s update.
  3. Update Main’s counter with the new model value we get back.
  4. Important. Use Cmd.map to wrap any Cmd values the update function might return. This gets it back into a type of Msg that Main understands.

The view

The view follows a similar, but simpler, pattern. To delegate to a view in a module, simply extract the html into a function and place it in the module.

    view model =
        div []
            [ Html.map CounterMsg (Counter.view model.counter) ]

Note that you need to use Html.map to wrap Counter.view’s return value. This is because Counter.view has return type Html Counter.Msg, but Main.view only understands Main.Msg values. Applying Html.map with an approprate Main.Msg constructor fixes this problem.


Finally, we need to delegate events from subscriptions. This, again, follows the same pattern.

    subscriptions : Model -> Sub Msg
    subscriptions model =
            [ Sub.map CounterMsg (Ports.somePort Counter.handleSomePort)

Here, port just refers to some JavaScript interop code you may or may not have. See my elm-interop post for more info.


This is just one way to solve the problem. The downside is that there is a decent amount of boilerplate. The upside, though, is that there is no magic.

Did this article help?

Sign up for my email newsletter because there's more helpful content on the way!

blog comments powered by Disqus


Sign up for my email newsletter because there's more helpful content on the way.


24 October 2016