6.9

Updating a Resource Using PATCH

We now know how to retrieve a resource from an HTTP server using the GET request. Most Elm apps also need to create, update, and delete resources on the server. In this section, we’ll learn how to update a post by using the PATCH HTTP method.

Saving the Updated Post

Right now, if we modify a post from the Edit Post page we built in the previous section, the new information is lost because we aren’t saving it anywhere.

Adding onInput Event Handler

The first order of business in making the Edit Post page functional is to add input handlers to text fields. Add onInput to all three text fields in the editForm function inside Views/Edit.elm.

editForm : Post -> Html Msg
editForm post =
    Html.form []
        [ div []
            [ text "Title"
            , br [] []
            , input
                [ type_ "text"
                , value post.title
                , onInput (UpdateTitle post.id)
                ]
                []
            ]
        , br [] []
        , div []
            [ text "Author Name"
            , br [] []
            , input
                [ type_ "text"
                , value post.author.name
                , onInput (UpdateAuthorName post.id)
                ]
                []
            ]
        , br [] []
        , div []
            [ text "Author URL"
            , br [] []
            , input
                [ type_ "text"
                , value post.author.url
                , onInput (UpdateAuthorUrl post.id)
                ]
                []
            ]
        , br [] []
        , div []
            [ button []
                [ text "Submit" ]
            ]
        ]

We’ll be sending a message to the Elm Runtime whenever the contents of a text field is modified. The message will contain the post ID and the modified content as payload. Although it appears that we are only sending the post ID, Elm will automatically pass the modified content as the second argument to the message constructor.

onInput is defined in the Html.Events module. Let’s import it in Views/Edit.elm.

module Views.Edit exposing (view)
.
.
import Types exposing (..)
import Html.Events exposing (..)

Defining Messages

Next we need to add those messages to the Msg type in Types.elm.

type Msg
    .
    .
    | LocationChanged Location
    | UpdateTitle Int String
    | UpdateAuthorName Int String
    | UpdateAuthorUrl Int String

The definitions above would read better if we’re explicit about what the Int type represents. Let’s define a type alias called PostId and use that instead of Int.

type alias PostId =
    Int


type Msg
    .
    .
    | UpdateTitle PostId String
    | UpdateAuthorName PostId String
    | UpdateAuthorUrl PostId String

Don’t hesitate to define as many type aliases as you need to improve the readability of your code. Let’s replace Int with PostId in the findPostById function in View.elm as well.

findPostById : PostId -> WebData (List Post) -> Maybe Post
findPostById postId posts =
    .
    .

Handling Messages

We’ll handle the messages above one at a time. The first one is UpdateTitle. Add a new branch to the update function in State.elm.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        .
        .
        UpdateTitle postId newTitle ->
            let
                updatedPosts =
                    updateTitle postId newTitle model
            in
                ( { model | posts = updatedPosts }, Cmd.none )

Also add the following definition for a new function called updateTitle to the bottom of State.elm.

updateTitle : PostId -> String -> Model -> WebData (List Post)
updateTitle postId newTitle model =
    let
        updatePost post =
            if post.id == postId then
                { post | title = newTitle }
            else
                post

        updatePosts posts =
            List.map updatePost posts
    in
        RemoteData.map updatePosts model.posts

Our strategy here is to find a post with the given ID and then update its title field. You maybe wondering why updateTitle doesn’t reuse the findPostById function (shown below) we created in the Routing to the Correct Page section to find a post. The reason for that is in addition to finding a post we also need to update and save it back to the posts field in our model.

findPostById : PostId -> WebData (List Post) -> Maybe Post
findPostById postId posts =
    case RemoteData.toMaybe posts of
        Just posts ->
            posts
                |> List.filter (\post -> post.id == postId)
                |> List.head

        Nothing ->
            Nothing

RemoteData.map

Let’s understand how the RemoteData.map function — used inside updateTitle — works.

RemoteData.map updatePosts model.posts

It applies the given function to the Success value stored in model.posts. If the value is anything other than Success, it ignores the function. Here is how its implementation looks:

map : (a -> b) -> RemoteData e a -> RemoteData e b
map f data =
    case data of
        Success value ->
            Success (f value)

        Loading ->
            Loading

        NotAsked ->
            NotAsked

        Failure error ->
            Failure error

The e and a type variables represent the Failure and Success values respectively as shown in RemoteData’s definition below.

type RemoteData e a
    = NotAsked
    | Loading
    | Failure e
    | Success a

updatePosts

updatePosts also uses map to apply yet another function to the posts, but the map function we’re using here is defined in the List module.

updatePosts posts =
    List.map updatePost posts

Transforming a value from one format to another is quite common in Elm. That’s why most modules provide their own implementation of the map function. Although conceptually similar, RemoteData.map and List.map operate on different types of data. RemoteData.map expects its second argument to be a Success value whereas List.map expects its second argument to be a List.

updatePost

The actual task of finding the right post and updating its title happens inside updatePost.

updatePost post =
    if post.id == postId then
        { post | title = newTitle }
    else
        post

The figure below illustrates how RemoteData.map, updatePosts, and updatePost functions work together to find a post and update its title.

Logging in Browser Console

It would be nice if we could find out whether or not the code we’ve written so far works. But until we have handled the remaining messages — UpdateAuthorUrl and UpdateAuthorRul— our app won’t compile. Let’s add two more branches to the update function in State.elm.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        .
        .
        UpdateTitle postId newTitle ->
            let
                updatedPosts =
                    updateTitle postId newTitle model
            in
                ( { model | posts = updatedPosts }, Cmd.none )

        UpdateAuthorName postId newName ->
            ( model, Cmd.none )

        UpdateAuthorUrl postId newUrl ->
            ( model, Cmd.none )

We’ll handle these messages properly in a moment. For now, we’ve a dummy implementation that simply returns the given model and an empty list of commands.

Our app will compile now, but how can we verify that the modified content from a text field is indeed saved in the title field? One approach is to log the new title in browser console.

The Debug module in Elm provides a function called log. That’s exactly what we need here. In addition to printing the output of unfinished code, we can use the Debug module to investigate bugs and performance problems. Let’s import it in State.elm.

module State exposing (..)
.
.
import Routing
import Debug

Now use Debug.log to print the value of newTitle in updateTitle.

updateTitle : PostId -> String -> Model -> WebData (List Post)
updateTitle postId newTitle model =
    let
        updatePost post =
            if post.id == postId then
                { post | title = Debug.log "newTitle: " newTitle }
            .
            .

Here is how Debug.log’s type signature looks:

Make sure elm-live is running and go to http://localhost:8000/posts/1. Open browser console and modify the title. You should see a new entry for each keystroke.

Opening browser console
Instructions for opening the browser console depends on which browser you’re using. Please read this nice tutorial from WickedlySmart that explains how to open the console on various browsers.

Now that we know our code is working, we can remove Debug.log from updateTitle. Don’t forget to remove the line that imports Debug module as well.

Saving New Author Name and URL

The code for properly handling UpdateAuthorName and UpdateAuthorUrl messages looks very similar to how we handled UpdateTitle. Modify the branches for those messages in the update function inside State.elm as shown below.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        .
        .
        UpdateAuthorName postId newName ->
            let
                updatedPosts =
                    updateAuthorName postId newName model
            in
                ( { model | posts = updatedPosts }, Cmd.none )

        UpdateAuthorUrl postId newUrl ->
            let
                updatedPosts =
                    updateAuthorUrl postId newUrl model
            in
                ( { model | posts = updatedPosts }, Cmd.none )

Also add the following definitions for updateAuthorName and updateAuthorUrl to the bottom of State.elm.

updateAuthorName : PostId -> String -> Model -> WebData (List Post)
updateAuthorName postId newName model =
    let
        updatePost post =
            if post.id == postId then
                let
                    oldAuthor =
                        post.author
                in
                    { post | author = { oldAuthor | name = newName } }
            else
                post

        updatePosts posts =
            List.map updatePost posts
    in
        RemoteData.map updatePosts model.posts


updateAuthorUrl : PostId -> String -> Model -> WebData (List Post)
updateAuthorUrl postId newUrl model =
    let
        updatePost post =
            if post.id == postId then
                let
                    oldAuthor =
                        post.author
                in
                    { post | author = { oldAuthor | url = newUrl } }
            else
                post

        updatePosts posts =
            List.map updatePost posts
    in
        RemoteData.map updatePosts model.posts

Unfortunately, Elm doesn’t provide an easy way to update a nested field in a record. We can’t use the dot syntax to modify a field like this:

{ post | author.name = newName }

This doesn’t work either:

{ post | author = { post.author | name = newName } }

A work around is to store the old author in a constant.

let
    oldAuthor =
        post.author
in
    { post | author = { oldAuthor | name = newName } }

Removing Duplication

There is quite a bit of duplicate code in updateTitle, updateAuthorName, and updateAuthorUrl. The only difference between them is the part that modifies the post record. Let’s extract that part out. Replace those three functions with the following code.

updateField :
    PostId
    -> String
    -> (String -> Post -> Post)
    -> Model
    -> WebData (List Post)
updateField postId newValue updateFunction model =
    let
        updatePost post =
            if post.id == postId then
                updateFunction newValue post
            else
                post

        updatePosts posts =
            List.map updatePost posts
    in
        RemoteData.map updatePosts model.posts


setTitle : String -> Post -> Post
setTitle newTitle post =
    { post | title = newTitle }


setAuthorName : String -> Post -> Post
setAuthorName newName post =
    let
        oldAuthor =
            post.author
    in
        { post | author = { oldAuthor | name = newName } }


setAuthorUrl : String -> Post -> Post
setAuthorUrl newUrl post =
    let
        oldAuthor =
            post.author
    in
        { post | author = { oldAuthor | url = newUrl } }

The branches for handling text field messages in update also contain duplicate code. We can remove the duplication by moving the code for modifying the posts field to updateField.

updateField :
    PostId
    -> String
    -> (String -> Post -> Post)
    -> Model
    -> ( Model, Cmd Msg )
updateField postId newValue updateFunction model =
    let
        updatePost post =
            if post.id == postId then
                updateFunction newValue post
            else
                post

        updatePosts posts =
            List.map updatePost posts

        updatedPosts =
            RemoteData.map updatePosts model.posts
    in
        ( { model | posts = updatedPosts }, Cmd.none )

Here’s how the branches for handling text field messages in update look now.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        .
        .
        UpdateTitle postId newTitle ->
            updateField postId newTitle setTitle model

        UpdateAuthorName postId newName ->
            updateField postId newName setAuthorName model

        UpdateAuthorUrl postId newUrl ->
            updateField postId newUrl setAuthorUrl model

That looks much cleaner. Check the output from elm-live in terminal to make sure we didn’t break anything.

Submitting the Updated Post to Server

Right now, the Submit button in Edit Post page doesn’t do anything. Let’s make it functional by adding onClick event handler to it in Views/Edit.elm.

editForm : Post -> Html Msg
editForm post =
    Html.form []
        .
        .
        , div []
            [ button [ onClick (SubmitUpdatedPost post.id) ]
                [ text "Submit" ]
            ]
        ]

Add SubmitUpdatedPost to the Msg type in Types.elm.

type Msg
    .
    .
    | UpdateAuthorUrl PostId String
    | SubmitUpdatedPost PostId

To handle this message properly, we need to first find a post with the given ID. If the post exists, we’ll create a command for submitting the post to our local server. If not, we’ll do nothing. Add the following branch to the bottom of update in State.elm.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        .
        .
        SubmitUpdatedPost postId ->
            case findPostById postId model.posts of
                Just post ->
                    ( model, updatePostCommand post )

                Nothing ->
                    ( model, Cmd.none )

We have already implemented the findPostById function in View.elm. It would be great if we could reuse it here. One way we can achieve that is by importing the View module in State.elm, but that creates unnecessary coupling between our state and view code. We should keep them separate as much as possible. A better alternative is to extract findPostById out to a separate module. Create a new file called Misc.elm in PostApp directory and add the code below to it.

module Misc exposing (..)

import Types exposing (..)
import RemoteData exposing (WebData)


findPostById : PostId -> WebData (List Post) -> Maybe Post
findPostById postId posts =
    case RemoteData.toMaybe posts of
        Just posts ->
            posts
                |> List.filter (\post -> post.id == postId)
                |> List.head

        Nothing ->
            Nothing

The Misc module provides a home for all the miscellaneous helper functions used throughout our app. Right now it only has one function. We may add more to it in the future. Import the Misc module in State.elm.

module State exposing (..)
.
.
import Routing
import Misc exposing (..)

Now that we’ve moved findPostById to Misc.elm, we can safely delete it from View.elm. We also need to import the Misc module in View.elm.

module View exposing (..)
.
.
import Views.Edit
import Misc exposing (..)

Creating a Command for Submitting Post

If you look at the elm-live window in terminal, you’ll see a compiler error: Cannot find variable updatePostCommand. Let’s fix it by adding the following code to the bottom of Rest.elm.

updatePostCommand : Post -> Cmd Msg
updatePostCommand post =
    updatePostRequest post
        |> Http.send PostUpdated

To keep things simple, we’re using Http.send here instead of RemoteData.sendRequest. We’ll define updatePostRequest in a moment. First let’s add the PostUpdated message to Msg type in Types.elm.

type Msg
    .
    .
    | SubmitUpdatedPost PostId
    | PostUpdated (Result Http.Error Post)

We need to import Http in Types.elm.

module Types exposing (..)
.
.
import Navigation exposing (Location)
import Http

Since we’re using Http.send to create a command, the payload inside PostUpdated will be of Result type instead of WebData. Let’s handle the PostUpdated message inside update in State.elm.

update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
    case msg of
        .
        .
        SubmitUpdatedPost postId ->
            ...

        PostUpdated _ ->
            ( model, Cmd.none )

Once again to keep things simple, we’re ignoring the response from server by returning the unmodified model and an empty list of commands. Properly handling the response is left as an exercise. If you don’t remember how to deal with errors, please see the Handling HTTP Errors section.

Now that we’ve taken care of the message, let’s add the code for creating an HTTP request to the bottom of Rest.elm.

updatePostRequest : Post -> Http.Request Post
updatePostRequest post =
    Http.request
        { method = "PATCH"
        , headers = []
        , url = "http://localhost:5019/posts/" ++ (toString post.id)
        , body = Http.jsonBody (postEncoder post)
        , expect = Http.expectJson postDecoder
        , timeout = Nothing
        , withCredentials = False
        }

Unfortunately, the Http module doesn’t provide a function called patch, so we’re forced to construct our request using a low-level function called Http.request. The Http.getString and Http.get functions we saw earlier in this chapter also use Http.request behind the scenes.

getString : String -> Request String
getString url =
    Http.request
        { method = "GET"
        , headers = []
        , url = url
        , body = Http.emptyBody
        , expect = Http.expectString
        , timeout = Nothing
        , withCredentials = False
        }


get : String -> Decode.Decoder a -> Request a
get url decoder =
    Http.request
        { method = "GET"
        , headers = []
        , url = url
        , body = Http.emptyBody
        , expect = Http.expectJson decoder
        , timeout = Nothing
        , withCredentials = False
        }

Understanding Http.Request

Http.request takes a record as an argument. Let’s go through each field in that record to understand what they represent.

method - To update a resource on the server, we need to use the PATCH method.

headers - The headers field allows us to send additional information to the server. Since we don’t want to send any headers, we’re giving it an empty list.

url - The location of the resource we want to modify.

body - The contents of the text fields in Edit Post page is assigned to this field. But first we need to translate that info from Elm values to JSON. Elm provides a module called Json.Encode for that. Let’s import it in Rest.elm. While we’re at it, let’s expose updatePostCommand too.

module Rest exposing (fetchPostsCommand, updatePostCommand)
.
.
import Json.Encode as Encode

We can now create encoders for post and author. Add the following code to the bottom of Rest.elm.

postEncoder : Post -> Encode.Value
postEncoder post =
    Encode.object
        [ ( "id", Encode.int post.id )
        , ( "title", Encode.string post.title )
        , ( "author", authorEncoder post.author )
        ]


authorEncoder : Author -> Encode.Value
authorEncoder author =
    Encode.object
        [ ( "name", Encode.string author.name )
        , ( "url", Encode.string author.url )
        ]

Encoding JSON values is the exact opposite of decoding. For comparison, here is how the decoders for post and author would look if we were to create them using the Json.Decode module:

postDecoder : Decoder Post
postDecoder =
    map3 Post
        (field "id" int)
        (field "title" string)
        (field "author" authorDecoder)


authorDecoder : Decoder Author
authorDecoder =
    map2 Author
        (field "name" string)
        (field "url" string)

We can’t assign the encoded value directly to the body field though. We need to explicitly tell Http.request that our encoded value is in JSON format by using the Http.jsonBody function.

body = Http.jsonBody (postEncoder post)

This will add the Content-Type: application/json header to our HTTP request behind the scenes. That is how the server knows that the body of a request is in JSON format.

expect - By using the Http.expectJson function we’re letting Elm know that we expect the response body to be JSON as well. We’re using the same decoder we created in the Decoding Nested Objects section to decode the response.

expect = Http.expectJson postDecoder

timeout - Sometimes a server takes forever to return a response. If we don’t want our users to wait too long, we can specify a timeout like this:

timeout = Just (Time.second 30)

timeout expects a Maybe. That’s why we need to wrap the value in Just. Since we don’t want to specify a timeout, we’re simply passing Nothing.

timeout = Nothing

withCredentials - This field takes a boolean. It indicates whether or not the credentials managed by a browser should be used to make cross-origin requests. Our server doesn’t require us to authenticate in order to access resources. That’s why we’ve assigned False.

withCredentials = False

The only thing remaining is to expose updatePostCommand in State.elm.

module State exposing (..)
.
.
import Rest exposing (fetchPostsCommand, updatePostCommand)
.
.

Check the elm-live window in terminal to make sure everything compiled successfully. Now go to http://localhost:8000/posts/1 and modify the title. After that click the Submit button.

Click Back to return to the list view and you should see the modified title.

You can also check server/db.json if you want to verify that the title was indeed modified by our request.

{
  "posts": [
    {
      "id": 1,
      "title": "json-server (modified)",
    .
    .
}

Summary

In this section, we learned how to modify a resource on our local server using the PATCH HTTP method. Along the way we figured out how to save changes from a text field in our model. We leveraged the Debug module to see the output of our unfinished code by printing a value in browser console. We saw what an HTTP request is really made up of by understanding each field present in the record given to HTTP.request. Finally, we learned how to encode Elm values into JSON using the Json.Encode module.

In the next section, we’ll delete a resource by sending a DELETE HTTP request.

Back to top

New chapters are coming soon!

Sign up for the Elm Programming newsletter to get notified!

* indicates required
Close