I recently wrote a navigation app for a hack day, and since it's written in Elm, and there aren't enough blog posts about Elm, I thought I'd break down some of the pieces. Here's a nice juicy one: letting the user search for a location, and turning that into latitude & longitude - 'geocoding'.
Let's see how Elm tackles it...
Every Elm app breaks down into a few simple parts:
Let's break those pieces down:
Our state of the world, our Model
, will need to capture the contents
of the form, the list of matching places, and the chosen
place. And for UX-sweetness we'll add in a loading-flag, so we can
disable the form while we're fetching results.
In Elm-land we'll model this as a record. First we'll need to define
the type of a search result, which I'll call a Place
:
type alias Place =
{address: String
,latitude: Float
,longitude: Float}
Now we can define the whole app state:
type alias Model =
{searchTerm : String
,loading : Bool
,places : Maybe (Result Error (List Place))
,chosenPlace : Maybe Place}
chosenPlace
is a Place
, of course, but initially no-one's chosen
anything, so we'll wrap it in a Maybe
.
places
is the trickiest type to read here. Maybe (Result
Error (List Place))
is saying:
Maybe
We may or may not have...Result Error ...
the result of a request, which may be failure, or may
be...List Place
a list of places.That's it. That's the state of the world, the whole state of the world, and will fully describe the state of the world for the lifetime of the app1.
By convention, Elm describes all events that could occur in the system
with an Action
type. What are the events that can occur in our app?
That translates into a union type of:
type Action
= ChangeTerm String
| Submit
| PlacesResponse (Result Error (List Place))
| ChoosePlace Place
For those unfamiliar with the syntax, this is saying that an Action
can be any one, and only one of the values that follow. Some, like
ChangeTerm
carry a payload value of String
, some like Submit
are
just markers.
That's it. Those are the events, all the events, and Action
fully
describes every event that can occur for the lifetime of the
app2.
The next piece we need is an event handler, which we call
update
. Let's start with an easy, but incomplete version:
update : Action -> Model -> (Model, Effects Action)
update action model =
case action of
ChangeTerm s -> ({model | term <- s}, Effects.none)
This is saying, "if you give me an Action
and the current Model
of
the world, I can process that Action
and give you the new Model
,
plus any side-effects that need to occur (like kicking off an AJAX
request)."
In this case, we're just handling the "user types into the input box"
case, to get used to the syntax. We unpack the string that the
ChangeTerm
action holds, and update the model with it. This case
causes no side-effects, so we say that explicity with Effects.none
.
The other easy event is "user chooses a place". Let's add that into our
update
function's case
statement:
case action of
...
ChoosePlace p -> ({model | chosenPlace <- Just p}, Effects.none)
...
The next up is submit:
case action of
...
Submit -> ({model | loading <- True}
,Effects.map PlacesResponse (findPlaces term))
...
That's more interesting. We're kicking off a search. In the model, we
set the loading
flag. This is purely for UX. Alongside that, for the
first time, we have an interesting side-effect. We're calling
findPlaces
to create a request, and then using map
to wrap the
response in a PlacesResponse
Action
, so that we'll know know what
kind event it is when we come to process it later.
We'll see how findPlaces
is defined later. For now, let's finish off
update
by defining how to handle the response:
case action of
...
PlacesResponse xs -> ({model | places <- Just xs
, loading <- False}
, Effects.none)
...
When we get a response, switch that loading
flag off, and store the
places.
That's it. That's the business logic, all the business-logic, and fully describes how every event changes the state of the world for the lifetime of the app3.
This seems like a lot, so let's take a breather. We have events. We have a state of the world. We have a function that moves the state of the world forward.
It's nice that the events follow pretty directly from asking, "What can the user do?"
It's also nice that once we've describe how to handle each event, we're done with business logic.
In the next release of Elm, the compiler will be able to warn you if you miss out any event handlers4, and that will be very nice indeed.
So now we just needed to describe that findPlaces
function, and
worry about the UI. To do that we'll need to talk about Effects
,
JSON-parsing and HTML-rending.
Why don't you go and make a cup of tea5 and join me for part two.
In the meantime, you can play with the finished hackday app here and read the source code to learn more.