We will build a simpler version of this in this blog post. It goes through the steps of setting up an Elm
project and the benefits of using Elm
on the front-end and how it provides some really nice help for you as a developer.
Prerequisites
You need a few things to get started. Knowledge of JavaScript, HTML and CSS is expected as this guide skips over all those steps and I recommend following my devcontainer guide and using an Elm
container (instead of a Node
container like that guide) since that eliminates any sort of installation process for you. For this tutorial I will be using a devcontainer, but if you want to install Elm
on your local filesystem you can do so too by following the steps here. Knowing the basics of some functional programming concepts won't hurt either 😊.
The Elm architecture
Elm
uses something it refers to as The Elm Architecture
. This architecture is really useful when dealing with transformations of data and reactive updates. Perhaps you've heard of a popular state management library called Redux? The Redux architecture is inspired by the one used in Elm. This pattern is built into Elm
, so no need for a third-party lib.
The Elm Architecture builds on three core principles:
- Model - The model (often called state in other frameworks) of your application
- View - Convert the model to HTML
- Update - Update the model
You can already see how this architecture works: The model turns into HTML that is then acted upon (updated) which then triggers an update and the HTML is re-rendered etc. You can think of this as a render loop.
Creating a basic page
Let's start by initializing the project, this is done by running the following:
Copyelm init
This creates a folder called src
and a file called elm.json
. You can think of this like the package.json
for a node
project, basically it describes your project and its dependencies.
Let's create our first page, create a page called Main.elm
and place it in the src
directory.
Copy-- Don't worry about this line, we can expose everything because
-- we will only be using this file, so exposing something doesn't
-- matter
module Main exposing (..)
-- Imports that we need the run our initial example
import Browser
import Html exposing (Html, div, text)
-- Initialize the empty record, this is used by Elm
-- to create the initial model
init : Model
init =
Model
-- We don't have a type annotation here, which is normally
-- recommended but that's okay because we don't have to worry
-- about what this function returns
main =
Browser.sandbox { init = init, update = update, view = view }
-- Right now, our Model is just an empty Record
type alias Model =
{}
-- Prepare the Msg type that will be used to trigger
-- updates in the render loop
type Msg
= Never
-- Right now, whatever Msg we get we won't update the model
-- Msg -> Model -> Model simplified means this function takes
-- in a Msg and a Model and produces a new Model.
update : Msg -> Model -> Model
update msg model =
Model
-- Our view is a function that takes the model and produces Html
-- and messages that in turn trigger the update function
view : Model -> Html Msg
view model =
div [] [ text "Hello world" ]
Now that we have our src/Main.elm
file set up, let's try to run our code and view it in a browser. We'll use elm reactor
for this. elm reactor
allows you to inspect your program in a file browser in the browser and whatever file you click on gets compiled and rendered, try it out by visiting http://localhost:8000 and browsing to src/Main.elm
after running:
Copyelm reactor
This should show you an HTML page that only contains the string Hello world.
Adding an Input field and tying it to our Model
Our page is useless at the moment. All it does is display a static string and since we want to create a todo tracker, let's start by adding an input field where we can type our todos. Modify the view
function to look like this instead:
Copyview model =
div []
[ input [] []
]
Also make sure you add input
as an exposed field near the start of the file:
Copyimport Html exposing (Html, div, input, text)
These HTML functions that correspond to HTML element names are functions that take two parameters, one list of attributes and one list of children, so our example with the updated model would generate HTML equal to this:
Copy<div>
<input />
</div>
Now, our input
can't really do anything, so we need to tie some behavior to it. Let's create a type of Msg
for that, call it UpdateInput
. Change the type Msg
we created earlier to look like this:
Copytype Msg
= UpdateInput String
We also need to let our Model
know that we need to track the state of a field:
Copytype alias Model =
{ value : String }
The Elm
compiler will complain now, because the type of our Model
changed, so our init
function needs to handle this new type of model:
Copyinit =
{ value = "" }
And let's have our input
fire the event when it's changed, let's also create a new div
element to display whatever the value of our model.value
is:
Copy-- Make sure to import the onInput event near the top of the file
import Html.Events exposing (onInput)
-- ...
view model =
div []
[ input [ onInput UpdateInput ] []
, div [] [text model.value]
]
Now we need to let our update
function react to this type of Msg
:
Copyupdate msg model =
case msg of
-- Whenever we get the UpdateInput message, assign the new value to our state's
-- value and set that to be the new value. Whatever update returns is the new
-- state of the application
UpdateInput input ->
{ value = input }
Run your app again and you should see that whatever you type into the input
field is reflected on a line below.
Creating a type for our Todo
We've come pretty far already, our next step should be preparing the type
needed by our application to track Todo items.
Let's create a Todo alias type, this let's you treat the type Todo as a record consisting of the fields within:
Copytype alias Todo =
{ id : Int, task : String, isComplete : Bool }
Our model should track the Todos, so that we can display them:
Copytype alias Model =
{ value : String, todos : List Todo }
Again, the power of static typing will give two handy errors for us to fix, since our Model
now contains one more field, we need to update both the init
and update
function:
Copyinit =
-- Let the todo list be empty to start with
{ value = "", todos = [] }
-- ...
update msg model =
case msg of
UpdateInput input ->
-- This is syntax sugar to let Elm know that when
-- this message is fired, the model should stay
-- unmodified except for the value property that
-- should be set to the new value of the input field
{ model | value = input }
That's great. We've added a new type and handled the addition of it. You can't interact with it at all yet though, let's make that happen in the next section.
Adding Todos to our state
First, we need a button we can press so that the value of our input is added as a todo in our Todo list.
Let's start by defining a new type of message:
Copytype Msg
= UpdateInput String
| AddTodo
You'll notice our code has another error now. This is the beauty of Elm
. You should see a compiler error telling you the following:
This is amazing, the compiler is letting us know that we haven't handled the case of the AddTodo
message and Elm
has no idea what to do if that was to happen, so it's forcing us to deal with it immediately. Let's do that:
Copyupdate msg model =
case msg of
UpdateInput input ->
{ model | value = input }
AddTodo ->
case model.value of
-- If the value of the input field is empty, do nothing
"" ->
model
-- Whatever else the input field is set to, add it as a todo
_ ->
{ model
| todos =
-- Let's update our todos field to include the new Todo item.
-- Its ID will be the number of todos at creation, its value
-- will be the value of our input and it will initially not
-- be marked as complete
{ id = List.length model.todos, task = model.value, isComplete = False }
-- (::) is a function in Elm that can be used to add something to the
-- front of a list
:: model.todos
}
Good. We're handling all the messages our app can pass at this stage. We're never firing the AddTodo
message at the moment, let's implement that next and add a way to display our list of Todos:
Copy-- Make sure button is imported at the top for this step
import Html exposing (Html, button, div, input, text)
-- And make sure the onClick event is included too
import Html.Events exposing (onClick, onInput)
--...
-- Create a helper function to render our todos
viewTodo : Todo -> Html Msg
viewTodo t =
div [] [ text t.task ]
view model =
div []
[ input [ onInput UpdateInput ] []
-- When we click the Add button, fire the AddTodo event
, button [onClick AddTodo] [ text "Add" ]
-- Run all our todos through the viewTodo function
, div [] (List.map viewTodo model.todos)
]
Run your app now and you should be able to type text into the input field and click add and you should see an increasing number of todos being added to the list.
Summary
Elm
is an excellent programming language in my opinion. The compiler really helps you out with extremely friendly error messages that tell you what you need to do to make it compile. Once it compiles, it's probably going to work.
You can achieve close to no runtime errors by using Elm. Sure, writing the code up front might take a bit longer but Elms compiler really helps you when you need to refactor or fix bugs. Elm
doesn't even have a concept of null
! Everything must have a value.
I think the barrier of entry to Elm
is a big high, but if you have some basic functional programming experience and knowledge of HTML, CSS and JavaScript you should be able to create web apps with Elm
.
The repository for this app is here: https://github.com/simon-nystrom/basic-elm-todo.
I hope you found it interesting and that you'll try out Elm
yourself!