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.
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.
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 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
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
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.