7.4

Creating Post Module

Let’s create a new directory called post-app inside beginning-elm instead of cluttering the src directory with too many modules.

We need to add post-app to the source-directories list in elm.json.

{
    .
    .
    "source-directories": [
        "src",
        "post-app"
    ],
    .
    .
}

Creating the Post Module

Let’s move the Post type and the code for decoding it from DecodingJson.elm to a new module. Create a new file called Post.elm inside the post-app directory and add the code below to it.

module Post exposing (Post, postDecoder, postsDecoder)

import Json.Decode as Decode exposing (Decoder, int, list, string)
import Json.Decode.Pipeline exposing (required)


type alias Post =
    { id : Int
    , title : String
    , authorName : String
    , authorUrl : String
    }


postsDecoder : Decoder (List Post)
postsDecoder =
    list postDecoder


postDecoder : Decoder Post
postDecoder =
    Decode.succeed Post
        |> required "id" int
        |> required "title" string
        |> required "authorName" string
        |> required "authorUrl" string

Using Custom Types for ID

Currently, the id field in Post is an integer. We could mistakenly pass an identifier of a different data structure, which also happens to be an integer, to a function expecting a post id. The Elm compiler won’t be able to catch a bug like that. We can help the compiler by implementing identifiers as custom types. Add the following code below the Post type alias in Post.elm.

type PostId
    = PostId Int

PostId is an opaque type. Let’s expose it in the module definition.

module Post exposing (Post, PostId, postDecoder, postsDecoder)

Change the id field’s type in Post from Int to PostId.

type alias Post =
    { id : PostId
    .
    .

PostId Decoder

Now that id is a custom type, simply using the int decoder in postDecoder won’t be enough. We need to create a custom decoder. Add the following code to the bottom of Post.elm.

idDecoder : Decoder PostId
idDecoder =
    Decode.map PostId int

We used the Decode.map function to convert an integer to PostId. Here is what its type signature looks like:

map : (a -> value) -> Decoder a -> Decoder value

Now we can replace int with idDecoder in postDecoder.

postDecoder : Decoder Post
postDecoder =
    Decode.succeed Post
        |> required "id" idDecoder
        .
        .

Converting PostId to String

The viewPost function in DecodingJson.elm uses String.fromInt to convert an id to a string.

viewPost : Post -> Html Msg
viewPost post =
    tr []
        [ td []
            [ text (String.fromInt post.id) ]
            .
            .

Now that the id field holds a value of a custom type that’s not going to work. We need to define a custom function for that. Add the following code to the bottom of Post.elm.

Note: Although we’re moving away from DecodingJson.elm, we’ll still be using the viewPost function in a module responsible for fetching and displaying posts in the next section.

idToString : PostId -> String
idToString (PostId id) =
    String.fromInt id

idToString uses pattern matching to expose the underlying integer value inside the PostId data constructor. Without this ability, we’d be forced to use a case expression which is not as elegant.

idToString : PostId -> String
idToString postId =
    case postId of
        PostId id ->
            String.fromInt id

Finally we need to expose idToString in the module definition.

module Post exposing (Post, PostId, idToString, postDecoder, postsDecoder)

In the next section, we’ll create a separate page for fetching and listing all posts in our database.

Back to top
Close