Introduction
Since the last article I wrote two years ago, we have been using Elm in production since then with great success.
This article is an update from the original article (Elm 0.18 -> Elm 0.19)
You might as well follow the official Elm guide.
What is Elm and why should I care?
Elm is a Web application development platform, providing a programming language, a compiler, an architecture and tooling. It focuses on making sure your app state and the HTML that reflects it are always in sync.
If you have been doing any frontend development during the last couple of years, you might have seen that the JavaScript ecosystem is a bit wild. Keeping up-to-date with the growing number of competing frameworks can take a lot of energy, not to mention the challenge in making sure your app works on all the available browser versions.
If you want to be able to write front-end applications without having to cope with the JavaScript fatigue, you should definitely try Elm.
Elm brings functional programming to your browser. The language is statically typed, so with Elm you won't see any runtime errors caused by inconsistent typing. Being functional and pure, Elm let you write code that is completely decoupled, which makes your code more reusable and easier to refactor.
How is Elm different?
Most of the time, using Elm, the two lines it takes to load your app from the HTML file is all the JavaScript you need. Your Elm code is compiled to JavaScript by the Elm compiler.
Most mistakes you make will be caught at compilation time, which means that you will barely ever encounter exceptions at runtime in the browser. Also, that means fewer unit tests and defensive programming to write, which is always good.
Because there is a compilation step, Elm forces you to handle every possible case of your model state. It makes sure you covered them on the rendering side.
Also, the Elm compiler is super smart. It always does its best to help you with meaningful messages, provides guidance to fix your types and handle uncovered edge cases. It even finds your typos!
Cannot find variable `nane`
13| hello ++ ", " ++ nane ++ "!"
^^^^
Maybe you want one of the following?
name
tan
Cmd.none
Sub.none
Detected errors in 1 module.
The tooling is great. elm-live automatically updates the browser code to reflect your code update in realtime. It comes with a debugger that shows you the list of events and state of your application at a given point in time — you can even go back in time and replay previous events. elm-format also formats your code automatically avoiding arguments over coding styles.
Getting started
Installing Elm
First of all install elm, elm-format and elm-live using npm — the nodejs package manager.
sudo npm install -g elm elm-format elm-live
If at this stage it doesn't work and you want to keep following, please use Ellie App to experiment: https://ellie-app.com/
Looking for an IDE?
Have a look at Atom. Combined with the appropriate extension, it will run elm-format each time you save your file – which is super handy.
Take a moment to make sure your text editor is configured to work well with Elm files.
Starting your first project
No need for boilerplate here, you can just start by running elm init to create the minimum elm.json file mandatory to start an app:
~/tutorial$ elm init
Hello! Elm projects always start with an elm.json file. I can create them!
Now you may be wondering, what will be in this file? How do I add Elm files to
my project? How do I see it in the browser? How will my code grow? Do I need
more directories? What about tests? Etc.
Check out <https://elm-lang.org/0.19.0/init> for all the answers!
Knowing all that, would you like me to create an elm.json file now? [Y/n]:
Okay, I created it. Now read that link!
You now have an elm.json file in your project as well as an empty src/ directory where you put your source files.
elm.json is to Elm projects what package.json is to nodejs ones.
~/tutorial$ tree -L2
├── elm.json
└── src
1 directory, 1 file
Creating your first file
To get started you can simply create a new file named src/Main.elm :
import Html
main = Html.text "Hello world"
Elm benefits from a full featured module system, with a broad ecosystem of external packages available. Html is part of the core libs.
Playing with elm-format
If your editor is well configured with elm-format, you should see this as soon as you save it:
module Main exposing (main)
import Html
main =
Html.text "Hello world!"
If not, you can run elm-format manually on your file:
~/tutorial$ elm-format --yes Main.elm
Opening your app in the browser
One way to run your app is to use elm reactor, the core app browser provided by the platform:
~/tutorial$ elm reactor
Go to <http://localhost:8000> to see your project dashboard.
Then open http://localhost:8000/src/Main.elm in your favorite Web browser.
Learning about the Elm virtual DOM
Virtual DOM functions to generate HTML are in the Html module.
The Html module we used above to render some text also exposes many more functions for rendering HTML tags. You can import them all using:
import Html exposing (..)
Note that unlike with some other languages, the Elm compiler will complain if you try to import symbols already defined in the current module, which makes it actually useful and really enjoyable to use.
You can then use text directly for instance:
main = text "Hello world"
The Virtual DOM HTML nodes are functions named after standard HTML tags, and take two parameters:
- A list of attributes
- A list of children
If I want to create a div with a link it would look like this:
module Main exposing (main)
import Html exposing (..)
import Html.Attributes exposing (..)
main =
div
[ class "container" ]
[ a
[ href "http://www.servicedenuages.fr/" ]
[ text "Blog" ]
]
We can also create a list of links in our div:
module Main exposing (main)
import Html exposing (..)
import Html.Attributes exposing (..)
main =
div
[ class "container" ]
[ ul
[ class "links" ]
[ li
[]
[ a
[ href "http://www.servicedenuages.fr/" ]
[ text "Blog" ]
]
, li
[]
[ a
[ href "http://www.elm-lang.org/" ]
[ text "Elm lang" ]
]
]
]
Adding some state
Now that you know how to render your page in HTML, let's see how to write a program that handles events.
A word about types
Before we dive into the core of an Elm program, I'd like to tell you about Elm types.
Every value in Elm has a type.
Elm itself defines the usual types, however, our business logic sometimes doesn't comply with the existing types.
To explain what types are, let's take a example that you use in every language without thinking about it: a boolean value.
In Elm, we would define the type like that: type Bool = True | False
In many other language we would call that an Enum.
The main difference is that in Elm, those enum value are patterns that can take parameters.
For instance, we will use a type to define the list of events that can be trigger with user interaction in our Elm program:
type Msg
= AddUser
| SetName String
| SetStatus UserStatus
type UserStatus
= Active
| Inactive
We define a custom type UserStatus that can be either Active or Inactive and a list of messages that will be triggered by button or radio clicks or keystroke in an input field for instance.
Here you see that our SetName event will take a String parameter.
If you want to know more about types, there is a paragraph below telling you more about it.
The Elm Architecture
The way Elm handles a program is by having:
- a Model that keep the state of the app,
- an update function that handles all the app events and updates the model state accordingly
- a view function that returns the Virtual DOM matching the state of the app every time it's updated.
For those who know Redux, it has been heavily inspired by Elm. Basically update is a reducer.
When with Redux you would write:
const initialState = 0;
const incrementAction = {
type: "INCREMENT"
};
const decrementAction = {
type: "DECREMENT"
};
function counterReducer(state = initialState, action) {
switch (action.type) {
case "INCREMENT":
return state + 1;
case "DECREMENT":
return state - 1;
default:
return state;
}
}
With Elm you will write:
type alias Model = Int
type Msg
= Increment
| Decrement
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
model + 1
Decrement ->
model - 1
The events and their parameters are defined in a Msg type.
In order to create our application that handle states, we can use the Browser package.
It provides four differents level of Elm programs.
- sandbox which is the simplest Elm program that you can build, it let you create a HTML element handled by Elm which, however, cannot communicate with the outside world.
- element create an HTML element managed by Elm.
- document which let you manage a HTML page (handle the title and the body tag)
- application which let you manage URL changes as well.
To start with, let's create our first Elm program using Browser.sandbox.
module Main exposing (main)
import Browser
import Html exposing (..)
type Msg
= NoOp
type alias Model =
{ name : String }
main =
Browser.sandbox { init = { name = "RĂ©my" }, view = view, update = update }
update : Msg -> Model -> Model
update msg model =
model
view : Model -> Html Msg
view model =
text ("Hello " ++ model.name)
We can now handle an event and change the name when we click on it.
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
type Msg
= Switch
type alias Model =
{ name : String }
main =
Browser.sandbox { init = { name = "RĂ©my" }, view = view, update = update }
update : Msg -> Model -> Model
update msg model =
case msg of
Switch ->
{ model | name = "SĂ©verine" }
view : Model -> Html Msg
view model =
div []
[ text "Hello "
, a [ href "#", onClick Switch ] [ text model.name ]
]
You can refresh the page and try it.
If we want to switch back to RĂ©my when we click on SĂ©verine we can add a if:
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
type Msg
= Switch
type alias Model =
{ name : String }
main =
Browser.sandbox { init = { name = "RĂ©my" }, view = view, update = update }
update : Msg -> Model -> Model
update msg model =
case msg of
Switch ->
if model.name == "RĂ©my" then
{ model | name = "SĂ©verine" }
else
{ model | name = "RĂ©my" }
view : Model -> Html Msg
view model =
div []
[ text "Hello "
, a [ href "#", onClick Switch ] [ text model.name ]
]
Enabling auto updates with elm-live
elm reactor is good to get started but if you want hot-reloading of your app, you might want to setup elm-live.
Once installed, run:
$ elm-live src/Main.elm
If you have to use the debugger, you can use the --debug option:
$ elm-live src/Main.elm -- --debug
It will automatically generate an index.html file with the compiled JavaScript, and open it in your default Web browser.
You can use the --output option to save the JavaScript in its own file and load it in the HTML yourself.
First update the index.html to make it looks like:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello world</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="elm.js"></script>
</head>
<body>
<div id="sandbox"></div>
<script>
var app = Elm.Main.init({node: document.getElementById("sandbox")});
</script>
</body>
</html>
Then you can run elm-live with the --output option:
$ elm-live src/Main.elm -- --debug --output elm.js
Now each time you will update your Elm code it will refresh the app in the browser.
Handling a second event
Let's add an input to let people choose who to greet.
module Main exposing (main)
import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
type Msg
= Switch
| NewName String
type alias Model =
{ name : String }
main =
Browser.sandbox { init = { name = "RĂ©my" }, view = view, update = update }
update : Msg -> Model -> Model
update msg model =
case msg of
Switch ->
if model.name == "RĂ©my" then
{ model | name = "SĂ©verine" }
else
{ model | name = "RĂ©my" }
NewName newName ->
{ model | name = newName }
view : Model -> Html Msg
view model =
div []
[ text "Hello "
, a [ href "#", onClick Switch ] [ text model.name ]
, br [] []
, input
[ onInput NewName
, value model.name
]
[]
]
The NewName event will be emitted with the content of the input each time we type in it.
Conclusion
That's about it. Now that you understand how the event update mechanism works and how you can define functions, you know more than you think about Elm.
When in doubt, the package documentation is really useful: https://package.elm-lang.org/
I hope you give Elm a shot on your next project and enjoy Elm as much as we do @Chefclub.
Wait a minute, That's it? Do I really know everything? But you didn't tell me how I was supposed to handle HTTP requests yet!
Handling HTTP requests
Fair enough, I remember asking exactly this question when I was introduced to Elm.
Let's use the photos collection of JSON Placeholder to get a list of JSON objects.
In order to do so we use the elm/http library.
The README is really enlightning already and I would recommand you to try to use it to create a fetchItems command.
Sending the request
The first thing is to create a command that we can trigger on the click of a button or during the init phase.
import Json.Decode as Decode
fetchItems : Cmd Msg
fetchItems =
Http.get
{ url = "https://jsonplaceholder.typicode.com/photos"
, expect = Http.expectJson GotItems (Decode.list decodePhoto)
}
Decoding the response
The expectJson tool is expecting a msg with a Result that can be either a Http.Error or the decoded items.
We can use type Msg = GotItems (Result Http.Error (List Photo)) to define the event.
Then we need to explain how we can build the Photo record from its JSON representation by writing a decoder.
import Json.Decode as Decode exposing (Decoder)
import Http
type Msg =
GotItems (Result Http.Error (List Photo))
type alias Photo =
{ id : Int
, title : String
, url : String
, thumbnailUrl : String
}
decodePhoto : Decoder Photo
decodePhoto =
Decode.map4 Photo
(Decode.field "id" Decode.int)
(Decode.field "title" Decode.string)
(Decode.field "url" Decode.string)
(Decode.field "thumbnailUrl" Decode.string)
At this stage you might be wondering what is this map4 thing and why on Earth we would use a function with the number of field that we want to decode.
I am glad you asked ;)
Let's rewind a little bit, there are two ways of creating a record:
Using its constructor:
newPhoto : Photo newPhoto = Photo 2 "Profile pic" "https://profile.nytimes.com/accounts/1.png" "https://profile.nytimes.com/accounts/thumbs/1.png"
By defining its fields:
newPhoto : Photo newPhoto = { id = 2 , title = "Profile pic" , url = "https://profile.nytimes.com/accounts/1.png" , thumbnailUrl = "https://profile.nytimes.com/accounts/thumbs/1.png" }
Decoders are using the constructor way to create records.
Decoding JSON values into records using Decode.map#
Decode.map# decodes each fields and then build a record using the constructor with each decoded values as a parameter. The position of the decoded fields is important and should be the same as the type alias definition.
Decoding JSON values into records using Decode.succeed
We can also create a record and use Decode.succeed to mark it as a decoded value. That's the API NoRedInk/elm-json-decode-pipeline is providing.
Using this, we can use pipes to iteratively define how our record should look like:
import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Pipeline exposing (required, optional, hardcoded)
decodePhoto : Decoder Photo
decodePhoto =
Decode.succeed Photo
|> required "id" Decode.int
|> required "title" Decode.string
|> required "url" Decode.string
|> required "thumbnailUrl" Decode.string
Even if it means installing one more dependency to your project, I would recommand using the later form that is more flexible when iterating on or refactoring decoders.
Note that in that case field order is also important, this will compose a decoder that in the end returns an object and in between return partial decoding functions.
Handling the response
Once the response body has been decoded, elm sends a message with a result that is handled by the update function.
A Result is a native Elm type that can either be a success or an error.
While defining a Result value, we give the type of the error and the type of the value. In our case: Result Http.Error (List Photo)
In our update function we need to handle both cases, when an error occured and when the photos list was decoded successfully.
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotItems result ->
case result of
Ok photos ->
( { model | error = Nothing, photos = photos }, Cmd.none )
Err err ->
( { model | error = Just <| errorToString err, photos = [] }, Cmd.none )
We can also write it a bit differently, which makes it more readable:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
GotItems (Ok photos) ->
( { model | error = Nothing, photos = photos }, Cmd.none )
GotItems (Err err) ->
( { model | error = Just <| errorToString err, photos = [] }, Cmd.none )
Showing the list of pictures
Once we are able to fetch our list of photos, we might want to display those pretty pictures.
I invite you to have a look at the Ellie with the fully functionnal version of the app: https://ellie-app.com/55LtWwHbhkPa1
The interesting part is the following:
displayPhotos : List Photo -> Html Msg
displayPhotos photos =
List.take 100 photos
|> List.map showPhoto
|> div []
showPhoto : Photo -> Html Msg
showPhoto photo =
a [ href photo.url, title photo.title ] [ img [ src photo.thumbnailUrl ] [] ]
List.map will take each photo of model.photos and create a list of the results of the showPhoto function.
Because showPhoto returns a Html Msg, List.map will return a list of Html Msg. We can then use this result as a list of children to a div [] element.
Conclusion
What's next? Starting from here, you can grow your widget. At some point you might want to create a Single Page App (SPA) and handle URL with multiple pages.
That's where the Elm Architecture really starts to shine. I would recommend you to have a look at elm-kitchen which will help you to get started with the scaffolding.
A word about Elm types and Elm records
Elm types
In Elm everything has a type.
- "hello" is a String
- 4 is a number
- 4.2 is a Float
Elm itself defines the usual types, however, our business logic sometimes doesn't comply with the existing types.
Let's think about a user, it can be Active or Inactive.
In other languages we would use an Enum, in Elm we can use a type.
type Status = Active | Inactive
You might want to use a boolean for this specific case, however using a type here does make your code more readable.
The benefit of use a type is that Elm is able to validate that you've handled all the possible cases.
If I want to display the status of my user I would write:
displayUser : User -> Html Msg
displayUser user =
div [] [ text <| user.username ++ " - " ++ statusToString user.status ]
statusToString : Status -> String
statusToString status =
case status of
Active ->
"This user is active"
Inactive ->
"This user is inactive"
But Elm types are also powerful Enum, because the possible cases can take parameters.
For instance, I can define a Msg like that:
type Msg
= AddTodo
| UpdateTodoDescription String
In that case my event UpdateTodoDescription will have a parameter of type String.
update : Msg -> Model -> Model
update msg model =
case msg of
AddTodo ->
{ model
| todos = Todo model.currentInputValue :: model.todos
, currentInputValue = ""
}
UpdateTodoDescription value ->
{ model | currentInputValue = value }
Elm records and type alias
If we want to define a user, we will create a record:
{ username = "Natim", status = Active }
The type annotation of this record can be deduced automatically by Elm and would be:
{ username : String, status : Status }
If I create functions that take a user, I would need to define what are the property of this user:
getUserName : { username : String, status : Status } -> String
getUserName user =
user.username
Instead of doing that, I can create a type alias to name the type annotation of my record:
type alias User =
{ username : String, status : Status }
getUserName : User -> String
getUserName user =
user.username
Because user.username and .username user are two acceptable ways of accessing the username property of our user, we can simplify our getUserName function like that:
getUserName : User -> String
getUserName =
.username
Elm types, a step further
Elm types can go a step further, let's look at composite types. The standard library already provide a bunch of them.
For instance Maybe, we can define maybe like that:
type Maybe a = Just a | Nothing
You might have seen the lowercase a here.
It is just to tell Elm that it can be any types. The only thing that matters is that the type defined should be the same as the type of the parameter of Just.
Maybe String will then be either a Just value with value of type String or a Nothing.
Maybe Int will then be either a Just value with value of type Int or a Nothing.
We have the same thing with Result that can be defined like that:
type Result a b = Err a | Ok b
We can then define Result Http.Error String that is either Ok value with value of type String or Err error with error of type Http.Error
We can even make complex types that are self-explanatory:
type Username = Username String
usernameToString = Username -> String
usernameToString (Username value) =
value
You might tell me, yes but it is much more handy to use:
type alias Username =
String
The only difference is that if you use type alias elm won't detect this kind of mistake:
type alias Username =
String
type alias FirstName =
String
type alias LastName =
String
type Status
= Active
| Inactive
type alias User =
{ firstName : FirstName
, lastName : LastName
, username : Username
, status : Status
}
createUser : Username -> FirstName -> LastName -> User
createUser username firstname lastname =
User username firstname lastname Inactive
Success! Compiled 1 module.
I don't know if you've seen the issue, but basically if we use our createUser function we will get the following record:
{ firstName = username, lastName = firstname, username = lastname, status = Inactive }
Which is not exactly what was expected.
This is because for Elm Username == FirstName == LastName == String.
While if we used:
type Username =
Username String
type FirstName =
FirstName String
type LastName =
LastName String
type Status
= Active
| Inactive
type alias User =
{ firstName : FirstName
, lastName : LastName
, username : Username
, status : Status
}
createUser : Username -> FirstName -> LastName -> User
createUser username firstname lastname =
User username firstname lastname Inactive
The compiler would have told us about the issue:
Detected errors in 1 module.
-- TYPE MISMATCH ------------------------------------------------------ Test.elm
The 3rd argument to `User` is not what I expect:
31| User username firstname lastname Inactive
^^^^^^^^
This `lastname` value is a:
LastName
But `User` needs the 3rd argument to be:
Username
Hint: I always figure out the argument types from left to right. If an argument
is acceptable, I assume it is “correct” and move on. So the problem may actually
be in one of the previous arguments!
-- TYPE MISMATCH ------------------------------------------------------ Test.elm
The 2nd argument to `User` is not what I expect:
31| User username firstname lastname Inactive
^^^^^^^^^
This `firstname` value is a:
FirstName
But `User` needs the 2nd argument to be:
LastName
Hint: I always figure out the argument types from left to right. If an argument
is acceptable, I assume it is “correct” and move on. So the problem may actually
be in one of the previous arguments!
-- TYPE MISMATCH ------------------------------------------------------ Test.elm
The 1st argument to `User` is not what I expect:
31| User username firstname lastname Inactive
^^^^^^^^
This `username` value is a:
Username
But `User` needs the 1st argument to be:
FirstName