On a very high level, web applications tend to have two major parts: state and user interface (UI). An application starts with an initial state and presents that state to the user through UI. The user takes some action through a UI element that modifies the initial state. The new state is then presented back to the user for more actions. The figure below shows the interaction between a state and UI in a hypothetical application that allows logged in users to create blog posts.
At any given point in time, an application needs to store different types of information in memory. For example, it needs to know whether or not a user is logged in or how many blogs a user has posted. State is like a repository for storing all of this information. This state is then made available to various data structures in the application. Functions in the application perform different operations on it, resulting into a new state.
In the Pure Functions section, we defined a state as something that represents all the information stored at a given point in time that a function has access to. Conceptually, an application state works the same way. It just contains a lot more information than a function-level state.
In Elm, we represent the state of an application with something called a model. A model is just a data structure that contains important information about the application. Imagine a simple app for incrementing and decrementing a counter. The only thing we need to track in this app is the current value of a counter. Here is what the model definition for this app looks like:
It’s just an
Int. A model doesn’t necessarily have to be complicated. It all depends on how complex the app is and how many different things it needs to track. For a simple counter app, all we need is a number that tells us what the current value is. A model is generally defined as a
Let’s add the above model definition to a file and start building a counter app in Elm. Create a new file called
Counter.elm in the
beginning-elm/src directory and add the code below to it.
The definition above doesn’t create a model. All it tells Elm is what our model looks like. Here is the code that actually creates our initial model:
Add the code above to the bottom of
Next, we need to present our initial model to the user. Add the following code to the bottom of
view function takes a model and returns HTML code. Behind the scenes, the
div function in Elm produces the
<div> element in HTML, and the
button function produces the
<button> element. The
text function doesn’t represent any HTML tag. It just displays a plain text by escaping special characters so that the text appears exactly as we specify in our code.
The first argument to
button represent a list of attributes. The second argument represents a list of nested elements. The Elm code in the
view function is equivalent to the following HTML code.
text functions are all defined in the
Html module included in the
elm/html package. The
Html module provides full access to HTML elements through normal Elm functions. Let’s import it in
Since we can treat HTML elements as plain old Elm functions, we can apply all the nice things Elm has to offer to the view code as well. For example, we can refactor the duplicate code out into separate functions and reuse them from different places in our app. We can write automated tests for the view code using the same tools used for testing any other Elm code. Elm compiler will even let us know if we made any mistake in the view code.
view function isn’t responsible for rendering HTML on a screen. All it does is take a model and return a chunk of HTML. It’s a pure function that returns the same HTML code given the same model. To actually render HTML on a screen, Elm uses a different package called
elm/virtual-dom behind the scenes. Don’t worry about how this package works now. We’ll explore it in detail in the Virtual DOM section later in this chapter.
elm/html package depends on
elm/virtual-dom. That’s why it was
automatically installed and listed as an indirect dependency in the
elm.json file when we ran
elm make in the Building a Simple Page with Elm section.
We don’t need to import any of the modules included in the
elm/virtual-dom package in our code because we shouldn’t be using it directly. It only exists to support the modules defined in the
view function’s type annotation suggests it returns a value of type
Html msg which means the HTML code generated by
view is capable of producing messages of type
msg. We’ll find out what
msg is in the Update section below.
Application Entry Point
Add the following code to the bottom of
main acts as an entry point for our app. All it does is pass the initial model to the
view function. Run
elm reactor from the
beginning-elm directory in terminal if it’s not running already and go to this URL:
http://localhost:8000/src/Counter.elm. You should see something like this in the top left corner of your browser:
Our app is utterly uninteresting at the moment. The buttons don’t do anything. That’s because we haven’t specified what should happen when the buttons are clicked. Let’s do that next. The first thing to do is define messages that represent the actions users can take. Add the following type definition right above
Msg is a custom type with two constants. When the
+ button is clicked, our app will receive the message called
Increment and clicking the
- button will generate the
- The term “message” doesn’t have any special meaning in Elm. It’s not a special type or a data structure. We could have very well called it “action” or “event” or “do this thing”. The official documentation and the Elm community prefer “message”, so we’ll go with that.
Model, we didn’t define
Msg as a type alias because there is no built-in type in Elm for representing messages. In contrast, our
Model is just an int value. So we made it a type alias that simply redefines the existing
To handle messages, we need to define a new function. Add the following code above
update function increments or decrements the model based on which message the app receives.
We’ve created a mechanism for handling messages, but we still don’t have a way to create them. Let’s take care of that. Modify the
view function to fire messages when the
- buttons are clicked.
Previously, the HTML returned by
view wasn’t creating any messages, so we just used
msg as a placeholder in its type annotation.
view uses the
onClick function from the
Html.Events module to generate messages of type
Msg. Let’s import
Msg is a concrete type, whereas
msg is just a type variable. A concrete type always starts with an uppercase letter. A type variable, on the other hand, starts with a lowercase letter. Unlike the special type variable
number we saw in the Type System section,
msg doesn’t have any special meaning in Elm. We could have used any random name to represent a generic message in the previous version like this:
Msg, we’ve made the
view function much more restrictive. Now, it can only generate two messages:
Decrement. Before, it could generate any message.
Wiring Everything Up
Here is how our
main looks right now:
view was enough when we were just displaying a static view. To allow interactivity, we need to add
update in the mix. Modify
main like this:
And import the
Browser module in
Browser module is included in the
elm/browser package which was installed automatically when we ran
elm make after our project was initialized in the Building a Simple Page with Elm section.
Elm Architecture essentially boils down to these three parts: model, view, and update. The entire application can be viewed as one giant machine that runs in perpetuity. The
Browser.sandbox function describes the structure of that machine: take an initial model, present it to the user, listen to messages, update the model based on those messages, and present the updated model back to the user again.
The diagram below illustrates the interaction between the Elm runtime and various components in our app.
Refresh the page at
http://localhost:8000/src/Counter.elm and you should be able to increment and decrement the counter.
main Function’s Type Annotation
Not sure if you noticed, but the
main function’s type annotation changed from
Html msg to
Program () Model Msg when we introduced the
Browser.sandbox function. Like any other function in Elm,
main takes the type of whatever expression it happens to return. We already know what
Html msg means.
Program () Model Msg means an Elm program that has a model of type
Model and accepts messages of type
Msg. The unit type
() indicates no values are passed to our app when it’s initialized. In chapter 8, we’ll see an example that shows how to pass values to an Elm program during initialization.
Model as a Domain Concept
You may wonder why do we even need to define
Model. We could simply replace
update’s type annotations like this:
And everything should work just fine. That is a valid point. However, by defining
Model, we’ve given a name to the value that flows through our app. This makes our code easier to read. In a simple app, the benefits of naming things properly aren’t huge, but in a large app, a well named domain concept such as
Model can add tremendous value from maintenance standpoint.
In this section, we learned the basic components of the Elm Architecture: model, view, and update. In the next section, we’ll find out what virtual DOM really is and what benefits we get from it. Here is the entire code for the counter app: