16 Apr 2016

Updated: 1st Aug 2016 for Elm 0.17.

With the rise of people getting to grips with Elm, I’m hear several folks wondering how to make the transition from play project to a larger app. So, in the hope that it helps ease that transition, here’s my guide to structuring large Elm apps. Follow along and you should have an easier time.

Startup

First, there are two packages that begin every Elm app, so let’s install them:

elm package install --yes elm-lang/core
elm package install --yes elm-lang/html

Then I’ll edit the elm-package.json file that’s just been created and set source-directories:

    "source-directories": [
        "src",
        "test"
    ]

With that in place, we can talk about file structure. There’s one special file, and a then a pattern that repeats throughout the app.

src/App.elm

The special file is the app’s entry point. I typically call it src/App.elm, though some choose src/Main.elm. If you were building several different Elm apps from the same code base, I might name the entry points more specifically, like src/Public.elm and src/Admin.elm. But for now, let’s just talk about a single entry point.

App contains as little as possible to get the app started, which is usually just:

module App exposing (main)

import Html.App
import State
import View


app : Program Never
app =
    Html.App.program
        { init = State.initialState
        , update = State.update
        , subscriptions = State.subscriptions
        , view = View.rootView
        }

This code is pretty much lifted from the docs. The only difference is this is /all/ just wiring. The real code has been moved out into a repeating pattern of four files.

The Featureful Four

Every feature, subfeature, page, widget and gizmo will have its own directory, and in that directory there will almost certainly be these four files:

.../feature/Types.elm

Types houses the type definitions for this feature. At a minimum, it will contain Model and Msg:

type alias Model = {...}

type Msg = ...

…plus any other types that are unique to this feature.

It will also house generic functions on those types. Not application logic, but library functions to make working with those types easier.

Types will typically export everything, and use anything from subfeatures’ Types, so the top of the file will look like:

module Feature.Types exposing (..)

import Widget.Types
import Grommit.Types

.../feature/State.elm

This is the brain of each feature. Shaped by Html.App, this will contain a minimum of three declarations, The init, update and subscriptions functions. First, init:

init : ( Model, Cmd Msg )
init = ...

This is what must be set up to start using this feature. Any parent that makes use of this feature agrees to pull this in when it starts using it.

It’s perfectly fine for init to be a function instead of a value. For example, you might have a reservation widget that expires after 15 minutes, so needs to know its startup time:

init : Time -> ( Model, Cmd Msg )
init startTime = ...

With the starting state in place, next we need the update function:

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

This is the function that reacts to events, triggers external effects and steps the the app forward in time. If you’re changing or debugging application logic, you’ll start working here.

Last we need subscriptions, which may be as simple as:

subscriptions : Model -> Sub Msg
subscriptions _ = ...

Or may actually do its own work:

subscriptions : Model -> Sub Msg
subscriptions model =
  Websocket.listen "ws://echo.websocket.org" Echo

Or even mix in the subscriptions of children:

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ Websocket.listen "ws://echo.websocket.org" Echo
        , Child.subscriptions model.child
            |> Sub.map ChildMsg
        ]

State will usually just export these three key components, and will expect them of its sub-components:

module Feature.State exposing (init,update,subscriptions)

import Feature.Types exposing (..)
import Widget.Types
import Widget.State
import Grommit.Types
import Grommit.State
...

.../feature/View.elm

View contains rendering code. Things with the type signature ... -> Html Msg, and rarely anything else. I name the entry point root:

root : Model -> Html Msg
root model = ..

View will usually only export root, and will use subfeatures’ Types model & View.root function:

module Feature.View exposing (root)

import Html.App as Html
import Feature.Types exposing (..)
import Widget.Types
import Widget.View
import Grommit.Types
import Grommit.View

...
grommitList model =
    Grommit.View.root model.grommits
        |> Html.map GrommitMsg
...

.../feature/Rest.elm

Last is RESTful code (HTTP and JSON-handling), which I name Rest. Not every feature needs such code, of course, but it’s so common that it’s worth describing.

The Rest namespace contains HTTP calls, and JSON encoders/decoders, and little else.

Rest will usually export everything, and may use subfeatures’ Types & Rest modules:

module Feature.Rest exposing (..)

import Feature.Types exposing (..)
import Widget.Types
import Widget.Rest
import Grommit.Types
import Grommit.Rest

Wrap Up

Along with the top-level App.elm, that pattern-of-four describes the vast majority of my directories in all of my Elm projects. As a layout it works well. The pattern makes everything easier to find and modify, as it answers a lot of useful questions without thinking:

  • Q. Where’s the code for registration?
  • A. Look for a directory called Registration.

  • Q. Where’s the code for feature X?
  • A. Look for a directory called X.

  • Q. X doesn’t look right.
  • A. Look in X/View.elm.

  • Q. X doesn’t behave right.
  • A. Look in X/State.elm.

  • Q. X needs a new feature.
  • A. Start by changing the data-model in X/Types.elm, and then follow the compiler messages through.

  • Q. Our API is changing.
  • A. Start by changing X/Rest.elm, and then follow the compiler messages through.

There’s very little surprise and a good default organization. The really nice thing about it is that it works recursively. You can use this layout in every feature and sub-feature and sub-sub-features.

src
├── App.elm
├── Types.elm
├── State.elm
├── View.elm
├── FrontPage
│   ├── Rest.elm
│   ├── State.elm
│   ├── Types.elm
│   └── View.elm
├── Login
│   ├── Rest.elm
│   ├── State.elm
│   ├── Types.elm
│   └── View.elm
└── Registration
    ├── Rest.elm
    ├── State.elm
    ├── Types.elm
    └── View.elm

Caveats

Take this pattern as a default, and remember to think for yourself. In my codebases you’ll find features that are so tiny that I occasionally stick the whole thing in one file. (Not often, but sometimes.) And you’ll certainly files other than these four, for special use-cases. But this simple approach covers a lot of ground, neatly.

comments powered by Disqus