Geocoding With Elm and ArcGIS, Part 2

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

AJAX

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.

JSON

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.

Decode one Place

decodePlace : Decoder Place
decodePlace = Place
  `map`   ("address" := string)
  `apply` (at ["location", "x"] float)
  `apply` (at ["location", "y"] float)

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

Right, That's It, We're Done

Oh wait, no. We haven't actually displayed anything to the user yet.

Rendering

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:

onClick

This makes a button click turn into a Submit action:

  ...
  onClick address Submit
  ...

disabled

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.

autofocus

For another UX nicety, it's trivial to make this form field capture the cursor when the page renders:

  ...
  autofocus True
  ...

on

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.

We're done

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:

  • Describing data
  • Describing events
  • Handling events
  • AJAX-y side-effects
  • Decoding JSON
  • React-style rendering

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.

Fin

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.

Footnotes


  1. And they're not paying me to say that. Well, actually, this app won a prize at the hackday, and ArcGIS were the sponsors, so maybe that does count as payment. :-D
  2. Thank you Richard Feldman for that analogy.
  3. Clojurians might find it interesting that this is very much like a 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.
  4. It's an approach that can work in many languages, but Elm makes the distinctions really clear.