elmcompositionelm-architecture

Composing Programs in the Elm Architecture


Suppose I want to create a webpage with two components, say a Navbar and a Body. These two components do not interact with each other and can be developed independently. So, I have two elm files which have the following components in each of them:

type Model = ...

type Msg = ...

init : (Model, Cmd Msg)

update : Msg -> Model -> (Model, Cmd Msg)

view : Model -> Html Msg

Assumming that they both work fine, how do we compose them to make a program which has both these components?

I tried writing something like this:

type Model = {body : Body.Model , navbar : Navbar.Model}
type Msg = BodyMsg Body.Msg | NavbarMsg Navbar.Msg

view : Model -> Html Msg
view model = div [] [Body.view model.body, Navbar.view model.navbar]

update : Msg -> Model -> (Model, Cmd Msg)
update = ...

The above gets ugly quickly when I try to write this update function. In particular, once I extract the Msg's out of the Cmd's update functions from Navbar.update or Body.update, how do I extract them and feed them back to these functions again? Also, the view function above does not particularly look idiomatic.

What is the elm-architecture recommended way to solve this problem? Is this pattern idiomatic in elm-architecture?


Solution

  • Yes, you're on the right path.

    In the view you need to use Html.map.

    view : Model -> Html Msg
    view model =
      div []
        [ Html.map BodyMsg (Body.view model.body)
        , Html.map NavbarMsg (Navbar.view model.navbar)
        ]
    

    Body.view model.body has type Html Body.Msg which requires us to use Html.map to get the correct type of Html Msg. Similarly for Navbar.view model.navbar.

    And, for the update function you'd write it like so:

    update : Msg -> Model -> (Model, Cmd Msg)
    update msg model =
      case msg of
        BodyMsg bodyMsg ->
          let
            (newBody, newCmd) = Body.update bodyMsg model.body
          in
            { model | body = newBody } ! [ Cmd.map BodyMsg newCmd ]
    
        NavbarMsg navbarMsg ->
          let
            (newNavbar, newCmd) = Navbar.update navbarMsg model.navbar
          in
            { model | navbar = newNavbar } ! [ Cmd.map NavbarMsg newCmd ]
    

    In the BodyMsg case, newBody has type Body.Model and so we can set the body field in model to it. However, newCmd has type Cmd Body.Msg and so before we can return we need to use Cmd.map to get the correct return type of Cmd Msg.

    Similar reasoning can be used for the NavbarMsg case.

    Also, the view function above does not particularly look idiomatic.

    What bothers you about the view code?

    N.B. This answer assumes you're using Elm 0.18.