The Structure of an Elm Application

Most languages when they are in their infancy, tend to be considered "toy languages" and are only used for trivial or small projects. But this is not the case with Elm, where its true power shines in complex and large applications.

It is not only possible to build some parts of an application in Elm and integrate those components into a larger JS application, but it is also possible to build the entire application without touching any other language making it an excellent alternative to JS frameworks like React.

In this article, we will explore the structure of an Elm application using a simple site to manage plain-text documents as an example.

Article Series:

  1. Why Elm? (And How To Get Started With It)
  2. Introduction to The Elm Architecture and How to Build our First Application
  3. The Structure of an Elm Application (You are here!)

Some of the topics covered in this article here are:

  • Application architecture and how things flow through it.
  • How the init, update and view functions are defined in the example application.
  • API communication using Elm Commands.
  • Single-page routing.
  • Working with JSON data.

Those are the principal topics that you will encounter when building almost any type of application, and the same principles can be extended to larger projects just by adding the needed functionality without significative or fundamental changes.

To follow this article is recommended to clone the Github Repository in your computer so you can see the whole picture; this article explains everything in a descriptive way instead of being a step by step tutorial talking about syntax and other details, so the Elm Syntax page and the Elm Packages site can be very helpful for specific details about the code, like type signatures of the functions used.

About the Application

The example that we will describe in this article is a CRUD application for plain-text documents, with communication to an API via HTTP to interact with a database. Since the main topic of this article is the Elm application, the server will not be explained in detail; it is just a Node.js fake REST API that stores data in a simple JSON file, using json-server.

In the application, the main page contains an input to write the title of a new document, and below a list of previously created documents. When you click a document or create a new one, an edit page is shown where you can view, edit and add content.

Below the title of the document, there are two links: save and delete. When save is clicked, the current document is sent to the server as a PUT request. When the delete click is clicked, a DELETE request is sent to the current document id.

Documents are created sending a POST request containing the title of the new document and an empty content field, once the document is created, the application changes to edit mode and you can continue adding the text.

A Quick Tip on Working with the Source Files

When you work with Elm–and probably with any other language–you will have to handle external details other than the code itself. The main thing you would want to do is to automatize the commands for development: starting the API server, starting Elm Reactor, compile source files, and starting a file watcher to compile again on every change.

In this case, I have used Jake, which is a simple Node.js tool similar to Make. In the Jakefile I have included all the necessary commands and one default command that will run the other ones in parallel, so I can just execute jake default on the terminal/command line and everything will be up and running.

If you have a bigger project, you can also use more sophisticated tools like Gulp or Grunt.

After cloning the application and installing dependencies, you can execute the npm start command, that will start the API server, the Elm Reactor, and a file watcher that will compile Elm files each time something changes. You can see the compiled files at http://localhost:3000 and you can see the application in debug mode (with Elm Reactor) at http://localhost:8000.

The Application Architecture

In the previous article we introduced the idea of the Elm architecture, but to avoid complexity we presented a beginner version with the Html.beginnerProgram function. In this application, we use an extended version that allows us to include commands and subscriptions, although the principles remain the same.

The complete structure is as follows:

Now we have an Html.program function that accepts a record containing 4 functions:

  • init : ( Model, Cmd Msg ): The init function returns a tuple with the application model and a command carrying a message, which allows us to communicate with the external world and produce side-effects, we will use that command to get and send data to the API via HTTP.
  • update : Msg -> Model -> ( Model, Cmd Msg ): The update function takes two things, a message with all the possible actions in our application and a model containing the state of the application. It will return the same thing as the previous function but with updated values depending on the messages that we get.
  • view : Model -> Html Msg: The view function takes a model containing our application state and returns Html able to handle messages. Usually, it will contain a series of functions that resemble HTML that renders values from the Model.
  • subscriptions : Model -> Sub Msg: The subscriptions function takes a model and returns a subscription carrying a message. Each time a subscription receives something it will send a message that can be caught in the update function. We can subscribe to actions that can happen at any time, like the movement of the mouse or an event in the network.

You can have as many functions as you want, but at the end, everything will return to those four ones.

Source: https://guide.elm-lang.org/architecture/effects/

Behind the scenes, the Elm Runtime is handling the flow of our application, all we do is basically define what are the things that flow. First, we describe the initial state of the application, its data structure and a command that gets initially executed, then the views are shown based on that data, on each interaction, subscription event or command execution, a new message is sent to the update function and the cycle starts again, with a new model and/or a command being executed.

As you can see, we don't actually have to deal with any control flow in the application, being Elm a functional language, we just declare things.

Routing: The Navigation.program Function

The example application is composed of two main pages, the home page containing a list of previously created documents, and an edit page where you can view the document or edit it. But the transition between the two views happens without reloading the page, instead, just the needed data is fetched from the server and the view is updated, including the URL (back and forward buttons still work as expected).

To achieve this we have used two packages: Navigation and UrlParser. The first one handles all the navigation part, and the second one helps us to interpret the URL paths.

The navigation package provides a wrapper for the Html.program function that allows us to handle page locations, so you can see in the code that we are using Navigation.program instead, that is basically the same as the previous one but also accepts a message that we have called UrlChange, which is sent every time the browser changes of location, the message carries a value of type Navigation.Location that contains all the information that we might need including the path, which we can parse to select the right view to show.

The Init Function

The init function can be considered the entry point of the application, representing both the initial state (model) and any command that we want to execute when the application starts.

Type Definitions

We begin by defining the types of values that we will be using, starting with the Model type, which contains all the state of the application:

type alias Model =
    { currentLocation : Maybe Route
    , documents : List Document
    , currentDocument : Maybe Document
    , newDocument : Maybe String
    }
  • We store a currentDocument value of type Maybe Route that contains the current location on the page, we use this value to know what to show on the screen.
  • We have a list of documents called documents, where we store all the documents from the database. We don't need a Maybe value here; if we don't have any documents we can have just an empty list.
  • We also need a currentDocument value of type Maybe Document.It will contain Just Document when we open a document and Nothing if we are on the home page, this value is obtained when we request a specific document from the database.
  • Finally, we have newDocument which represents the title of a new document in form of a Maybe String, being Just String when there is something in the input field, otherwise, it is Nothing. This value is sent to the API when the form is sent.

Note: It might look unnecessary to have that value here, coming from JavaScript you might think that you could just get the value directly from the input element, but in Elm you have to define everything in the model; when you enter something into the input element, the model gets updated and when you send the form, the value in the model is sent via HTTP.

As you can see, in Model we are also using other type aliases, that we have to define, being Document and Route:

type alias Document =
    { id : Int
    , title : String
    , content : String
    }

type Route
    = HomeRoute
    | DocumentRoute Int

First, we are defining a Document type alias to represent the structure of a document: an id value that contains an integer then the title and content, both being String.

We also create a union type that can group two or more related types, in this case, they will be useful for the navigation of the application. You can name them as you want. We only have two: one for the homepage called HomeRoute and another one for the edit view which is called DocumentRoute and it carries an integer that represents the id of the specific document requested.

Putting it Together

Once we have the types defined, we proceed to declare the init function, with its initial values.

init : Navigation.Location -> ( Model, Cmd Msg )
init location =
    ( { currentLocation = UrlParser.parsePath route location
      , documents = []
      , currentDocument = Nothing
      , newDocument = Nothing
      }
    , getDocumentsCmd
    )

After introducing the navigation package, our init function now accepts a value of type Navigation.Location, which contains information from the browser about the current page location. We store that value in a location parameter so we can parse and save it as currentLocation, we use that value to know the correct view to show.

The currentLocation value is obtained using the parsePath function from the Navigation package, it accepts a parser function (of type Parser (a -> a) a) and a Location.

The stored value in currentLocation has a Maybe type. For example, if we have a /documents/12 path in our browser, we would get Just DocumentRoute 12.

The parser function that we have called route is built like this:

route : UrlParser.Parser (Route -> a) a
route =
    UrlParser.oneOf
        [ UrlParser.map HomeRoute UrlParser.top
        , UrlParser.map DocumentRoute (UrlParser.s "document" </> UrlParser.int)
        ]

The most important parts being:

HomeRoute UrlParser.top

We basically create a relation, where HomeRoute is the type that we defined for the home route and UrlParser.top which represents the root (/) in the path.

Then we have:

DocumentRoute (UrlParser.s "document" </> UrlParser.int)

Where we have again a route type called DocumentRoute, and then (UrlParser.s "document" </> UrlParser.int) which represents a path like /document/<id>. The s function accepts a string, in this case document, and will match anything with document on it (like /document/…). Then we have a </> function that can be considered a representation of the slash character in the path (/), to separate the document part from the int value; the id of the document that we want to see.

The rest of our model consists of a list of documents, which by default is empty, although it's populated once the getDocumentCmd command finishes. There are also values for the current document and a new document, both being Nothing.

The Update Function

The update function works with a message and a model as input and a tuple with a new model and a command as output, usually, the output will depend on a message being processed.

In our application we have defined a message for each event:

  • When a new page location is requested.
  • When the page location changes.
  • When a new document title being entered in the input element.
  • When a new document is saved and created.
  • When all the documents in the database have been retrieved.
  • When a specific document is requested and retrieved.
  • When the title and content of a document that is being updated.
  • When a specific document is saved and retrieved.
  • When a document is deleted.

And this can be done using a union type:

type Msg
    = NewUrl String
    | UrlChange Navigation.Location
    | NewDocumentMsg String
    | CreateDocumentMsg
    | CreatedDocumentMsg (Result Http.Error Document)
    | GotDocumentsMsg (Result Http.Error (List Document))
    | ReadDocumentMsg Int
    | GotDocumentMsg (Result Http.Error Document)
    | UpdateDocumentTitleMsg String
    | UpdateDocumentContentMsg String
    | SaveDocumentMsg
    | SavedDocumentMsg (Result Http.Error Document)
    | DeleteDocumentMsg Int
    | DeletedDocumentMsg (Result Http.Error String)

Some messages need to carry some extra information, and it can be defined next to the message name, for example, the NewUrl message has a String attached containing a new URL path.

Also, most of the messages can be found in pairs, especially messages that add a new command to the Runtime, one message is before the command is executed and the other one after it gets executed.

For example, when you delete a document, you send a DeleteDocumentMsg message with the id of the document to be deleted, then once the document is deleted a DeletedDocumentMsg message is sent containing the result of the HTTP call: a status value Http.Error and the result as a String.

As we will see next, the messages containing the result of a command should be pattern-matched for both of its values, either as an error or as a success value.

Once we have all the messages defined, we can start working on what we will do with each one. For this, we pattern-match the message, let's take the reading of a specific document as an example:

ReadDocumentMsg id ->
            ( model, getDocumentCmd id )

This will match the ReadDocumentMsg message containing an int (as per our type definition) named id.

Note The name of the int value is assigned when it is matched, before it gets matched the value is just something of type Int.

Then we return a tuple containing the model without any changes, but we also return a command to be executed, called getDocumentCmd and receives the id of the document as an input. Do not worry about the command definition yet, we will get into it below.

Now we need to match the message that is sent once we get the requested document:

GotDocumentMsg (Ok document) ->
            ( { model | currentDocument = Just document }, Cmd.none )

GotDocumentMsg (Err _) ->
            ( model, Cmd.none )

Remember that the GotDocumentMsg message carried a (Result Http.Error Document) value, so we have to match for its two possible values: if it succeeds and if it fails.

The first case here will match if the error is type Ok, meaning that there was no error, and the second value will be the retrieved document. Then we can return the tuple containing a modified model where the currentDocument value is the document we just got, preceded by a Just, because currentDocument has a type of Maybe. Also, now in the second part of the tuple, we indicate that we will not execute any command (Cmd.none).

In the second case, where there was an error, we match it with a value of type Err and we can use an _ as a placeholder for anything that could be there. In a real world application we could show an information box to the user informing about the error, but to avoid complexity in this example, we will just simply ignore it; so we return the model again without any changes and also we don't execute any command.

All of the other message matches follow the same pattern: they return a new model with the information carried by the message and/or they execute a command.

API Communication with Commands

Although Elm is a pure functional programming, we can still perform side effects like communicating via HTTP with a server, and this is done using Commands.

As we have seen previously, every time a message is matched, we return a tuple containing a new model and a command. A command is any function that returns a value of type Cmd.

Let's take a look at the command that we included in our init function, which performs a request to the server with all the documents in the database when the application is starting:

getDocumentsCmd : Cmd Msg
getDocumentsCmd =
    let
        url =
            "http://localhost:3000/documents?_sort=id&_order=desc"

        request =
            Http.get url decodeDocuments
    in
        Http.send GotDocumentsMsg request

The two important parts of the function are its type declaration getDocumentsCmd : Cmd Msg and Http.send GotDocumentsMsg request in the in section.

The type means that it is a command and it carries a message too, it is obtained from the type that the Http.send function returns, which you can see in the package documentation.

In the body of the function, we can see the message that is sent once the request has been completed. For clarity purposes, we have created two variables: one with the URL of the API where the request is sent, and the other one containing the request itself that Http.send will send.

The request is built using the Http.get function, since we want to send a GET request to the server.

You can also notice a decodeDocuments function in there, it is a JSON decoder, we use it to transform the server response in JSON to an usable Elm value. We will see how the decoders used in this application are built in the next section.

The command to get a single document from the server is quite similar, since the Http.get function does most of the work for us to build the request. We just change the URL of the resource that we want, in this case using the id of the requested document.

But to send data to the server, the history is a little different; instead, we can build the request ourselves using the Http.request function.

Let's examine the function that sends a new document to the server:

createDocumentCmd : String -> Cmd Msg
createDocumentCmd documentTitle =
    let
        url =
            "http://localhost:3000/documents"

        body =
            Http.jsonBody <| encodeNewDocument documentTitle

        expectedDocument =
            Http.expectJson decodeDocument

        request =
            Http.request
                { method = "POST"
                , headers = []
                , url = url
                , body = body
                , expect = expectedDocument
                , timeout = Nothing
                , withCredentials = False
                }
    in
        Http.send CreatedDocumentMsg request

Again we have a function that returns a value of type Cmd Msg, but now we also take a value of type String, which is the title of the new document to be created.

Using the Http.request function, we pass a record as a parameter containing all the parts of the request, we are mainly interested in the following:

  • method: The HTTP method of the request, we previously used GET to get information from the server, but now that we are sending the information we use the method POST.
  • url: The API endpoint that receives the request.
  • body: The body of the request, containing the document that we want to add to the database in form of JSON. To build the body we use the Http.jsonBody function, that automatically adds a Content-Type: application/json header for us. This function expects a JSON value, that we produce using a JSON encoder and the title of the new article. We will see how the JSON encoder is implemented in the next section.
  • expect: Here we indicate how we should interpret the response of the request, in our case, we will get back the new document, so we use the Http.expectJson function to transform the response using our decodeDocument JSON decoder.

The Http.send function is practically the same as the one we mentioned previously; the only difference is that now we will send a CreatedDocumentMsg message once the document has been created.

The command to update a document is also very similar to the command to create a new document, the main differences being:

  • We send the data to a different API endpoint depending on the id of the document that we want to update.
  • The body is built with a complete document and is encoded to JSON using a different encoder.
  • The HTTP method used is PUT, which is the preferred method for making updates to existing resources.
  • We use the SavedDocumentMsg message once we receive a response.

Lastly, we have the deleteDocumentCmd command function. The principles still remain the same, but in this case, we will not send anything in the body of the request, so we use the Http.emptyBody. Also, we indicate that we expect a String value, but it does not really matter since we are not using it for anything in our application.

Working with JSON Values

In Elm, we can't use JSON directly in our code, nor we can use a simple parsing function like JSON.parse() as we do in JavaScript since we have to make sure that the data that we are handling is type-safe.

To use JSON in Elm, we have to decode the JSON value into an Elm value, and then we can work with it, we do this using JSON decoders. Also, the inverse is similar; to produce a JSON value we have to encode an Elm value using JSON encoders.

In our sample application, we have two decoders and two encoders. Let's analyze the decoders:

decodeDocuments : Decode.Decoder (List Document)
decodeDocuments =
    Decode.list decodeDocument


decodeDocument : Decode.Decoder Document
decodeDocument =
    Decode.map3 Document
        (Decode.field "id" Decode.int)
        (Decode.field "title" Decode.string)
        (Decode.field "content" Decode.string)

A decoder function has to have a type Decoder (which in this case is Decode.Decoder because of the way we imported the JSON package.) In the signature is also indicated the type of the data in the decoder, the first one is a list of documents so the type is List Document and the second one is simply a document so it has a Document type (we defined this type at the beginning of the application).

As you can notice, we are actually composing these two encoders, because the first one decodes a list of documents, we can use the document decoder for the Decode.list function.

It is in the decodeDocument decoder where the real thing happens. We use the Decode.map3 function to decode a value of three fields: id, title and content, with their respective types, the result is then put into the Document type we defined at the beginning of the application to create the final value.

Note: Elm has eight mapping functions to handle JSON values, if you need more than that you can use the elm-decode-pipeline package, which allows building arbitrary decoders using the pipeline (|>) operator.

Now we can see how the two encoders are implemented:

encodeNewDocument : String -> Encode.Value
encodeNewDocument title =
    let
        object =
            [ ( "title", Encode.string title )
            , ( "content", Encode.string "" )
            ]
    in
        Encode.object object


encodeUpdatedDocument : Document -> Encode.Value
encodeUpdatedDocument document =
    let
        object =
            [ ( "id", Encode.int document.id )
            , ( "title", Encode.string document.title )
            , ( "content", Encode.string document.content )
            ]
    in
        Encode.object object

To encode a JavaScript object, we use the function Encode.object which accepts a list of tuples, each tuple containing the name of the key and the encoded value depending on their type, Encode.int and Encode.string in this case. Also, unlike Decoders, these functions always return a value of type Value.

Because we are creating a document with an empty content, the first encoder only needs the title of that document and we manually encode an empty content field right before sending it to the API. The second encoder accepts a complete document and just produces a JSON equivalent.

You can see more functions related to JSON in the Elm Packages site: Json.Decode and Json.Encode

The View Function

The view function, compared with the code of previous articles, remains pretty straightforward. The interesting change here is the way we show each page depending on the URL path.

First of all, we have a link that always points to the home page, and the way we do this—instead of using regular links—is by capturing the click event and we send a NewUrl message with the new path.

Because we are still using regular <a> elements in our application instead of buttons, we have created a custom event called onClickLink, which is the same as the onClick event but preventing the default behavior (preventDefault) of the clicked element.

The implementation of this event is as follows:

onClickLink : msg -> Attribute msg
onClickLink message =
    let
        options =
            { stopPropagation = False
            , preventDefault = True
            }
    in
        onWithOptions "click" options (Decode.succeed message)

The important thing to note here is the use of the onWithOptions function, which allows us to add two options to the click event: stopPropagation and preventDefault. The option that does the trick here is preventDefault which prevents the default behavior of the <a> element.

Next, we have the implementation of the function that handles the page that is shown depending on the path in the URL:

page : Model -> Html Msg
page model =
    case model.currentLocation of
        Just route ->
            case route of
                HomeRoute ->
                    viewHome model

                DocumentRoute id ->
                    case model.currentDocument of
                        Just document ->
                            viewDocument document

                        Nothing ->
                            div [] [ text "Nothing here…" ]

        Nothing ->
            div [] [ text "404 – Not Found" ]

Remember that we are storing the current location in a currentLocation variable in the model, so we can apply pattern-matching to that variable and show something depending on its value. In our example, we first check if the Maybe value is of type Just Route or Nothing, then if we have a route, we check if it is a HomeRoute or a DocumentRoute. For the first case we include the viewHome function which represents the content of the homepage, and for the second case, we pass the currentDocument value in the viewDocument function, which shows the selected document.

For each document entry, notice in the viewDocumentEntry function that we are sending again a NewUrl message with the link to the respective document using the onLinkClick event. This message is responsible for loading the corresponding document.

Finally, we can add inline CSS in each component by adding a function of type Attribute, using Html.Attributes.style, which has the following form:

myStyleFunction : Attribute Msg
myStyleFunction =
    Html.Attributes.style
        [ ( "<property>", "<value>" )
        ]

In the example application, we have the styles of some of the components, and other generic styles directly included in the HTML file where the application is embedded. You can choose between including CSS files directly as you would normally do on any website, or you can write them directly within the Elm source files. While the method shown in this example is quite simplistic, there is a specialized library for this, in case you need more control: Elm-css.

A Word on Subscriptions

Subscriptions can be a common thing in a lot of applications, and although we didn't use subscriptions in this example, their mechanism is quite simple: they allow us to listen for certain things to happen when we do not know when they are going to happen.

Let's see the basic structure of a subscription:

subscriptions : Model -> Sub Msg
subscriptions model =
  WebSocket.listen "ws://echo.websocket.org" NewMessage

The first thing to mention is that all subscriptions have a type Sub, here we have Sub Msg because we are sending a message each time we receive something on the subscription.

The way this works is that the WebSocket.listen function creates a socket listener for the address ws://echo.websocket.org, and each time something arrives, the NewMessage message is sent, and in our update function, we can act properly to this message, as we have done previously (Thanks to the Elm Architecture).

Application Embedding

Now that we have seen how a complete application is constructed, it's time to see how we can include that application in an HTML file for distribution. Although Elm can generate HTML files, you can just generate JavaScript and include them by yourself, so you also have control over other things, like the styling.

In HTML you can include the following:

…
  <body>
    <main>
      <!-- The app is going to appear here -->
    </main>
    <script src="main.js"></script>
    <script>
      // Get the <main> element
      var node = document.getElementsByTagName('main')[0];

      // Embed the Elm application in the <main> element
      var app = Elm.Main.embed(node);
    </script>
  </body>
…

First, we include the compiled Elm file in .js in a <script> tag, then we get the element where the application is going to be rendered, in this case, the <main> element, and finally, we call Elm.Main.embed(<element>) where <element> is the HTML node we got previously.

And that's it.

Conclusion

Elm is a great alternative to JavaScript frameworks in building large web applications. Not only provides a default architecture to keep things in order but also provides all the nice things of a well-designed language for modern applications.

The topics covered in this article are found in most applications you will build, and enough information to get started building production sites, once you get used to these, the rest is just a matter of keep exploring.

Article Series:

  1. Why Elm? (And How To Get Started With It)
  2. Introduction to The Elm Architecture and How to Build our First Application
  3. The Structure of an Elm Application (You are here!)