As many of you probably do, I wanted to upgrade my Elm application to use the new elm/http 2.0 packages. So today I bit the bullet, and did just that. I thought it might be interesting for others, so here we go.
I used elm/http 1.0, NoRedInk/elm-rails and lukewestby/elm-http-builder throughout the existing code.
This was also an opportunity to streamline this .
elm.json
I moved the existing elm.json out of the way, and proceeded with elm init
to make a brand new one.
I did this because changing the file by hand tends to lead to a lot of issues.
I also remove elm-stuff, just to be sure.
elm install elm/http
(yes! version 2.0), and then installed all my other packages.
Code changes
I used a certain pattern a lot in my code. I would have a function that would return a Http.Request
that I would Http.send
in another module. It took me a while to find a way to keep this flexibility.
load: Int -> Http.Request Order
load id =
decoder
|> Http.get ("/orders/" ++ String.fromInt id)
init: Flags -> (Model, Cmd Msg)
init f =
(
{ order_id = f.id},
load f.id |> Http.send Loaded
)
In the same file, this is easy and it just becomes:
load: Int -> Cmd Msg
load id =
Http.get
{ url = "/orders/" ++ String.fromInt id)
, expect= Http.expectJson Loaded decoder
}
init: Flags -> (Model, Cmd Msg)
init f =
(
{ order_id = f.id},
load f.id
)
But when the load
function is in another module?
load : Int -> (Result Http.Error Order -> msg) -> Cmd msg
load id toMsg =
Http.get
{ url = "/orders/" ++ String.fromInt id)
, expect= Http.expectJson toMsg decoder
}
and in the main module:
init: Flags -> (Model, Cmd Msg)
init f =
(
{ order_id = f.id},
load f.id Loaded
)
That was not so bad.
The backend we are using in this app is a Rails application. Rails uses certain conventions, one is that for creating records you use POST
, for updating PATCH
or PUT
and DELETE
for destroying. Instead of figuring out how to do this I started using elm-rails (elm 0.17) and later on elm-http-builder.
I switched to elm-http-builder to be able to specify the CSRF token in the header. elm-rails requires me to install an extra npm
package to find the token in the Rails-generated HTML. I prefer to pass it to my app as a flag. But still there were some elm-rails calls all over the place.
Most of the time the changes were in line with the changes above:
decoder |> Rails.post "url" body |> Http.send Created
to
Http.post
{ url = "url"
, body =body,
, expect = Http.expectJson Created decoder
}
This was possible because those were posts where the CSRF verification is disabled. So better ignore those and send the token as seen below.
The places we used elm-http-builder usually looked something like
HttpBuilder.post "url"
|> withHeader "X-CSRF-Token" model.csrf
|> withJsonBody (encode model.form)
|> withExpect (Http.expectJson decoder)
|> toRequest
|> Http.send Created
Becomes
Http.request
{ method = "POST"
, url = "url"
, headers = [ Http.header "X-CSRF-Token" model.csrf ]
, body = Http.jsonBody (encode model.form)
, expect = Http.expectJson Created decoder
, timeout = Nothing
, tracker = Nothing
}
And exactly the same goes for PUT
, PATCH
and DELETE
.
RemoteData
Kris Jenkins has the formidable package krisajenkins/remotedata, see his talk: Slaying a UI Antipattern.
My code looked like this:
get : Cmd Msg
get =
Decode.list decode
|> Http.get "/accountabilities.json"
|> RemoteData.sendRequest
|> Cmd.map Loaded
and now looks like:
get : Cmd Msg
get =
Http.get
{ url = "/accountabilities.json"
, expect = Http.expectJson (RemoteData.fromResult >> Loaded) (Decode.list decode)
}
Remember the functions I used to make that would return a Http.Request
and now return a Cmd msg
?
load id
|> RemoteData.sendRequest
|> Cmd.map Loaded
now
load id (RemoteData.fromResult >> Loaded)
Returning an Int
EDIT
You may skip the next part, as @hector pointed out to me an Int is valid Json…
So the expect becomes simply expect = Http.expectJson Created Decode.int
.
But I’ll leave the code here, maybe somebody will find it interesting
/EDIT
There was still one snag, some of my actions return an integer value only, not a json, just an integer.
post : Model -> Cmd Msg
post model =
let
decoder : Decode.Decoder Int
decoder =
Decode.int
in
HttpBuilder.post "/addresses"
|> withHeader "X-CSRF-Token" model.csrf
|> withJsonBody (encode model.form)
|> withExpect (Http.expectJson decoder)
|> toRequest
|> Http.send Created
now becomes
post : Model -> Cmd Msg
post model =
Http.request
{ method = "POST"
, url = "/addresses"
, headers = [ Http.header "X-CSRF-Token" model.csrf ]
, body = Http.jsonBody (encode model.form)
, expect = expectInt Created
, timeout = Nothing
, tracker = Nothing
}
Wait, where is that expectInt
coming from?
expectInt : (Result Http.Error Int -> msg) -> Http.Expect msg
expectInt toMsg =
Http.expectStringResponse toMsg <|
\response ->
case response of
Http.GoodStatus_ metadata body ->
case Json.decodeString Json.int body of
Ok value ->
Ok value
Err err ->
Err (Http.BadBody (Json.errorToString err))
Http.BadStatus_ metadata body ->
Err (Http.BadStatus metadata.statusCode)
Http.BadUrl_ url ->
Err (Http.BadUrl url)
Http.Timeout_ ->
Err Http.Timeout
Http.NetworkError_ ->
Err Http.NetworkError
I hope this will be useful.
Herman