Following on from part 1, we had completed our description of the app, and the event handlers, and we just needed to worry about making an AJAX request, decoding the response, and rendering the HTML.
Just. :-)
ArcGIS has a nice API for geocoding1. We send a GET with the term as a query parameter, we get back some JSON for places in the world that match that search term. It's easy to use.
For our wiring, we have a search term, and we want to generate the side-effect that eventually returns a list of places. Remembering that that side-effect may fail with an HTTP error, we get this type signature:
findPlaces : String -> Effects (Result Error (List Place))
To read the actual implementation you'll need to know that |>
is an
operator that basically reads as a unix pipe |
2- take the results of
the last function and pass them in as the input to the next function:
findPlaces term =
"http://geocode.arcgis.com/arcgis/rest/services"
++ "/World/GeocodeServer"
++ "/findAddressCandidates?f=json&singleLine=" ++ Http.uriEncode term
|> Http.get decodePlaces
|> Task.toResult
|> Effects.task
There's a lot of type-wrangling in there, but the noteworthy bit is
that Http.get
takes a JSON-decoder as its first argument, which will
fire if the get request was successful.
Let's look at a sample response from the API:
{
"spatialReference": {
"wkid": 4326,
"latestWkid": 4326
},
"candidates": [
{
"address": "Royal Festival Hall",
"location": {
"x": -0.11599726799954624,
"y": 51.50532882800047
},
"score": 100,
"attributes": {},
"extent": {
"xmin": -0.120998,
"ymin": 51.500329,
"xmax": -0.110998,
"ymax": 51.510329
}
}
]
}
For JSON-decoding I always pull in [JSON extras](https://twitter.com/circuithub][CircuitHub]]'s [[http://package.elm-lang.org/packages/circuithub/elm-json-extra/latest/), so let's import those:
import Json.Decode as Json exposing (..)
import Json.Decode.Extra exposing (apply)
Now the parsing is reduced to two functions, which make use of :=
to
access a property and at
to walk a nested path down the tree.
Place
decodePlace : Decoder Place
decodePlace = Place
`map` ("address" := string)
`apply` (at ["location", "x"] float)
`apply` (at ["location", "y"] float)
List
of Places
decodePlaces : Decoder (List Place)
decodePlaces =
at ["candidates"] (list decodePlace)
At the actual hackday I wrote this code, I found this kind of thing very fast to write. You'd think having to write a parser for JSON before you can use it would slow you down, but certainly on that day I found it sped things up: The first pass was slower, sure, but as I built out the app and discovered that my initial assumptions about the schema were wrong, having the compiler tell me all the places my assumptions were wrong saved a lot of time.
Oh wait, no. We haven't actually displayed anything to the user yet.
So the last piece we need is a rendering function. HTML gets huge, so I'll just focus on an illustrative piece and point you to the source.
Most HTML-rendering functions in Elm have essentially the same signature. You might expect it to be as simple as:
searchForm : Model -> Html
...but it's slightly more complicated than that, because DOM.
It's not enough to just generate HTML. We also need a way for user
clicks and keypresses to generate events (as Elm Actions
). So the
most common type signature is some variant of:
searchForm : Address Action -> Model -> Html
That Address Action
will give us a way to make onClick
events
generate a Action
, without polluting our rendering code with
business logic. It's a channel, queue, or postbox where we can send
event data3.
So, let's look at our search box. (Here's a primer for reading Elm's HTML syntax.)
searchForm address model =
div [class "form-group"]
[input [class "form-control"
,autofocus True
,on "keyup" targetValue (message address << TermChange)
,type' "text"] []
,hr [] []
,button [class "btn btn-lg btn-block btn-success"
,disabled model.loading
,type' "submit"
,onClick address Submit]
[text "Search"]]
Let's look at a few of those pieces:
This makes a button click turn into a Submit
action:
...
onClick address Submit
...
Here we disable the button when there's a load in-flight. It's an easy way to give the user some feedback that the click actually caused something:
...
disabled model.loading
...
...recall that model.loading
is handled in the update
function. The button is not made responsible for tracking which AJAX
requests are in-flight, as that would be an insane abuse of the single
responsibility principal. It's just a button that displays how it's told.
For another UX nicety, it's trivial to make this form field capture the cursor when the page renders:
...
autofocus True
...
The only really juicy part of this code is the input keyup
event
handler. It's a little more complex because it needs to refer to
the value of the event target. It's also interesting because it shows
how you can handle any arbitrary event type:
...
on "keyup" targetValue (message address << TermChange)
...
Whenever there's a keyup
event, grab the element's value, wrap it up
as a TermChange
Action
, and send it on its merry way.
That's it. A complete overview of a Geocoding module for an app written in Elm. It feels like we covered a lot of pieces:
And that is a lot of pieces, but any app would need all of them somewhere. The nice thing about this approach is that each piece is discrete - you can think about it and write it in isolation, and then glue them together later4. You can also easily split the writing of them, so that coders who are good with HTML can focus on the rendering. I am muchly fond of this approach in general, and Elm's implementation of it specifically. Especially when it's a hack day and I need to move fast without breaking things.
The whole app is a little larger, and I'll be writing up other pieces. In the meantime, you can see the geocoding part in action by playing with the finished hackday app here and then read the source code to learn more.
core.async
channel, except it's one-way. You can only write to an Address
, and it has a counterpart on the other side that can only be read from.↩