Introduction to The Elm Architecture and How to Build our First Application

Creating our first Elm application might seem like a hard task. The new syntax and the new paradigm can be intimidating if you haven't used other functional programming languages before. But once you create your first application, you will understand why Elm has been gaining so much attention lately.

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 (You are here!)
  3. The Structure of an Elm Application

In this article, we are going to build a simple application that will introduce us to the language fundamentals: the Elm architecture and how to start making things. It is going to be pretty simple, but it will introduce us to one of the most common tasks in any application: reacting to user actions and doing something with them inside the application.

A stylized version of the Elm application for this tutorial

It is going to be composed of a form to add bit pieces of text and to filter previous entries. A list of all the entries is also included. There is a GitHub repo for all the code we go over in this article.

Although this is targeted to complete Elm beginners without any experience with the language, we assume that you have already read the basic things about it, like the syntax to write functions and how to apply them. To learn more about this, you can read the dedicated Syntax page on the Elm documentation site.

Introducing the Elm Architecture

Every Elm application tends to follow a particular pattern to the point that it is now considered the way to write Elm. If you are familiar with the MVC architecture, you will find some familiarities in the concepts but if the concepts are entirely new to you don't worry, once we build this application everything is going to be more clear.

In most applications, we are going to find these three essential parts:

  • Model: The model function stores all of our application state; which is all the dynamic data that is moved around in our application during its execution.
  • Update: The update function contains other functions that make your application dynamic. We use these to process our state using the logic necessary to accomplish the goal of the application.
  • View: The view function handles the visual part of our application. Using smaller functions that resemble HTML, we can build the structure of the application embedding data from the model. We can also include calls to events that will trigger certain functionality.

Hello, World!

Building a dynamic application with Elm can be considered simpler than start building one with vanilla HTML/JS. To begin, we just need one file. We don't need to think about what other libraries we are going to need.

Go ahead and create one file with a .elm extension in some directory for this project, we are going to refer to it as main.elm although you can name it whatever you like. Once you get the file open in your favorite editor, add the following content, which is a simple "Hello, World!" application that will help us to get started before building the real one:

import Html exposing (text)

main =
  text "Hello, World!"

First, we import the Html module that allows us to write the visible application structure with something similar to HTML and we expose the text function which makes it possible to output plain text on the site.

As in other programming languages, the main function is the entry point of the application; it is the first one that is executed after starting the program. What we are doing here is to pass the "Hello, World!" string to the text function as a parameter, then assign it to the main function. Once the application is executed, it will show a simple "Hello, World!" text in the browser.

To see the result in the browser, we have to compile the code first. As mentioned in the article Why Elm? (And How To Get Started With It), the Elm Platform includes the Reactor utility which allows us to see our Elm in our browser compiling the code automatically.

In a terminal/command line window, execute the following command inside the directory containing the Elm file that you just created:

elm-reactor

It will start a development server, and we can access to it entering http://localhost:8000 in the browser.

The Reactor File Navigation Window after the project has been built

Once in the browser, you will see a File Navigation Window. Click on the main.elm file and wait a couple of seconds while it gets built. In the process it will also create an elm-package.json file which is the equivalent of the package.json file in node.js projects; it contains essential information about your application including the dependencies, in this case, obtained directly from our application file. It will also download those dependencies and place them inside an elm-stuff directory. Once the process is completed, you will see the text "Hello, World!" on the page.

Now that we have confirmed that everything is working, it is time to start building our application.

Adding Modules and Defining the Entry Point

To use certain functions, we first have to import the modules that contain them. Replace the content of your file with the following:

module Main exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)

The first line defines our application as a module called Main that will expose everything on it. Although we won't use the module externally, it is important to know about it. You can read more about modules in the Module page from the site An Introduction to Elm.

The following three lines import some modules that we are going to use across our application. The first one is the one that we used in the Hello World example, Html.Attributes allows us to use HTML attributes in our Elm code, and Html.Events make available common events like mouse or form actions.

In this case, we are exposing everything inside those components, so if at some point in your work, you find any conflict with names, you can specify the exact components that you want to import.

The next thing is to define the entry point of our application, add the following code to your file:

main =
    Html.beginnerProgram { model = model, view = view, update = update }

For simple applications, it is common to use the Html.beginnerProgram function, it helps us to put together our program which is composed of those three principal components as mentioned before: the model, the view and the update.

The repetition of the names might seem a little redundant to you at first (model = model, etc.) but that’s because we are going to name our model function as model, our view as view and update as update, as is usually done (and recommended) in other Elm programs, but it isn't a strict rule; you can also have something like:

main =
    Html.beginnerProgram { model = mydata, view = myhtml, update = mylogic }

But if you decide to do this in your program, just remember to name your function definitions accordingly.

Model

In our model, we are going to put the state of our entire program, so first, we have to think about the things that we are going to store and use.

We can describe Model as a record of:

  • entries: A list string with all entries.
  • results: A list of strings with the filtered entries.
  • filter: A string containing the filter.

And to indicate that type of information in our Elm program, we are going to create what is called a type alias for our model function, as it name indicates, it will allow us to represent a complex data structure in a simpler way.

Include the following code below the main function that you defined previously:

type alias Model =
    { entries : List String
    , results : List String
    , filter : String
    }

As you can see, the code is quite expressive — not too different from the description we created before — just a bit more concise. Now we can use Model (Types are always capitalized) as a type for our functions without having to write all the structure manually; this is why we use type aliases, to make things cleaner, easier to understand and less repetitive.

Although types can be inferred, it’s a good practice to write them before writing the actual function; it helps us to think and to get into the right place from the beginning. Also, the compiler will tell us if we do something wrong inside the function.

Now we can write our function which will contain the information we just defined, put the following in your file:

model : Model
model =
    { entries = []
    , results = []
    , filter = ""
    }

The first line tells Elm that our model function is of type Model, which is the type we defined previously, and next we define the actual function. But because our application starts without any data, we just include empty lists ([]). Those values are going to change once the user starts interacting with the application.

Strictly speaking, in Elm, values don't change (they are immutable), instead new values are created, although that's mostly transparent to us.

That's it! Now our application's state is represented in a central place, but we still need to implement the functions that are going to work with that state and make the internals of the application work.

Update

All the logic in our program is handled by a single function named update and this function will take something called a [message] — which we can think of as a container for actions that we can react to — and also will take our current model, returning a new model with our changes depending on the action applied.

Our application will have two actions, one to enter items into the string list from the form input and another one to filter those entries. To define this in Elm, we can add the following to our file:

type Msg
    = Filter String
    | Add

Here we are defining a new union type called Msg to represent our messages, each message can be either a Filter action with a String attached to it containing the text from the filter or an Add action that we are going to use to trigger the addition of the filter (the string in the form input) to the list of entries.

Now we have the Msg type ready to be used within our update function; we can proceed to add the following in our file:

update : Msg -> Model -> Model
update msg model =
    case msg of                                                                                                                
        Filter filter ->
            { model
                | results = List.filter (String.contains filter) model.entries
                , filter = filter
            }

        Add ->
            { model
                | entries = model.filter :: model.entries
                , results = model.filter :: model.results
            }

Although it might look a little complex, it is composed of very simple parts to achieve the functionality of our application.

The first line is the type definition of our update function. It says that it is going to take a Msg and a Model and will return a Model because we are going to update our model on each message depending on its content.

Next, we define the actual function which is going to have msg and model as parameters that we are going to use inside the function. First, we have to check the type of action that we have to handle using a case function, which is Filter and Add as we defined previously.

The Filter action takes a string which we are naming filter, and it will update our model, changing the results field with a new list product of filtering (List.filter) our entries that contain (String.contains) the filter string. We also update the value of model.filter with the content of the filter, so we can access it as we need. You can check the syntax for updating records in the Syntax documentation.

The next case is Add which is going to add the filter string from the model to the entries and results fields, this will happen every time the user clicks an "Add" button, which we are going to define in the next section. Adding the string to the entries list is self-explanatory, but we are also adding it to the list of results because in this way we will be able to see it immediately after the Add message is sent, otherwise we would have to delete part of the string and write it again to see the addition.

And that would be everything regarding the logic of our application, the only thing we now have to do is to define how it will be shown in the browser.

View

So far we have defined the internal parts of our application; everything that happens behind the scenes. But we still have to define how we are going to show all the information to the user and how they can interact with it.

The interface for the application is going to be composed of three main elements: the form input where the user can enter a filter string to search or to add it to the entries, an Add button to add the text to the entries and the list of entries.

In HTML we would have something like this:

<div>
    <input placeholder="Filter…" oninput="Filter()">
    <button onclick="Add()">Add New</button>
    <ul>
        <li>Item list</li>
        <li>…</li>
    </ul>
</div>

But we can't write HTML directly in Elm. We have to use specialized functions that mimic the HTML tags. To accomplish this, add the following to your file:

view : Model -> Html Msg
view model =
    div []
        [ input [ placeholder "Filter…", onInput Filter ] []
        , button [ onClick Add ] [ text "Add New" ]
        , ul [] (List.map viewEntry model.results)
        ]

First, we define the type of our view function, which is going to have a Model as input containing all the data and Html Msg as output with is the HTML representation that will be shown in the browser. The body of the function is almost a direct representation of our HTML code but with a different syntax in the form of <tag> [<attributes>] [<content>].

Notice how we are sending a Filter message on each onInput event, which happens every time the user enters something into the input element. And we do something similar in the Add message with the onClick event.

For the list of entries, we map over the elements of the mode.results list using List.map, and we pass each entry to the viewEntry function that we are going to define like this (add it to your file):

viewEntry : String -> Html Msg
viewEntry entry =
    li [] [ text entry ]

It is just a simple helper function to have a cleaner code in view. It accepts a string as entry and returns it inside a li element with the type Html Msg.

And now our application is finished. You can just reload your browser window (make sure Reactor is still executing), and you will see the final result.

Our Elm application while a filter is applied

Conclusion

The example included in this article is pretty simple, but hopefully, it will help you through the initial curve of learning the Elm programming language. If you want, you could try to expand this example into a more complex application; it will help you to continue learning without having to deal with the "how do I start" problem which is common with new technologies.