WebApps UI with Elm
Feb 7, 2016I first stumbled upon Elm when I was learning Redux but didn’t really paid attention until I encountered it again last week and thought i’d read a little more this time. To my surprise, the syntax was like Haskell and its even functional as well.
Whenever I’m learning a new language its always a good idea to try implementing some sort of small application with it to see how it feels. In this case I implemented the popular TodoMVC in Elm. You can grab the finished project here and I’ll go over quickly on some of the interesting things I learned along the way.
Like Redux which was inspired by Elm, Elm has employs this uni-directional data-flow. It does this through The Elm Architecture, which means each Elm program should be broken up into three parts:
- Model
- Update
- View
This is like the MVC pattern but instead of calling the business logic Controller
its called Update
but the idea is still the same. With this separation its clear what certain part of your program does since its clearly defined which makes it easier for testing and debugging as well among other things.
In the Todo app, we have the following models defined:
type alias Todo =
{ id: Int
, name: String
, isCompleted: Bool
}
type alias Model =
{ todos: List Todo
, leftCounter: Int
, appliedFilter: Filter
, completedCounter: Int
, todoInput: String
, nextId: Int
, isCompletedAll: Bool
}
In Elm, these are called Records, they’re just like json objects except there’s no methods and prototypes, just plain records. So why do we have two models for a simple Todo? The first is our actual Todo item, the second is the model for our whole app. Whereas before you’d store these as state managed individually by each component, in Elm you put all data in a single place and let each field be consumed by only interested components. If you read my react native post, this is the language that inspired all that.
Next is update and part of it is our actions:
type Action
= NoOp
| Add
| UpdateTodoInput String
| ToggleCompleted Int
| Delete Int
| ToggleCompletedAll
| SelectFilter Filter
This is how you clearly define all possible actions that could happen from within your component. For actions that accepts arguments, its also clearly defined.
Now lets see how our update function might look like:
update : Action -> Model -> Model
update action model =
case action of
NoOp ->
model
...
Delete id ->
let
nonDeletedTodos = List.filter (\todo -> todo.id /= id) model.todos
completedCounter = List.length <| List.filter .isCompleted nonDeletedTodos
leftCounter = (List.length nonDeletedTodos) - completedCounter
in
{ model | todos = nonDeletedTodos
, leftCounter = leftCounter
, completedCounter = completedCounter
}
At the very top of our function is called type annotation, it defines the signature of our function. Here we’re saying that our update function accepts an action and a model then returns a new model. The annotation might look confusing at first since its hard to tell which ones are the params and the return values, just keep in mind the last item is always the return value.
We have a couple of cases defined here, one for each Action but I omitted the others for brevity. So this is how you handle update, since each interaction produces an action, you can use that to determine what changes should happen. We simply use a case statement which is like switch from other languages, notice how Delete
is implemented.
In Elm all data are immutable, so instead of plucking the concerned todo out of our list of todos, we simple filtered out the non-deleted todos which gives us a new list minus the deleted one. Then we use that to compute the counters for completed and remaining todos. If you’ve never worked with functional language before, this would seem odd but its a really powerful pattern.
Since data must be immutable, we return a whole new model where the todos and counters has been replaced. And this is how you set the value from a Record
, using the pipe (|
) operator, underneath the hood it clones the Record and creates a new one where the incoming changes is applied as opposed to update inline.
And what about the view? well it gets even easier, you no longer need to manually write html tags since you can define them declaratively:
getTodoItem : Address Action -> Todo -> Html
getTodoItem address todo =
li
[ classList [ ("completed", todo.isCompleted) ] ]
[ div
[ class "view" ]
[ input
[ class "toggle"
, type' "checkbox"
, checked todo.isCompleted
, onClick address <| ToggleCompleted todo.id
]
[]
, label [] [ text todo.name ]
, button
[ class "destroy"
, onClick address <| Delete todo.id
]
[]
]
, input
[ class "edit"
, name "title"
, id <| "todo-" ++ (toString todo.id)
]
[]
]
Every Html node in Elm accepts two params, first is a list of element attributes and next is a list of child nodes. Here we have two children for our li
element which is a div
and an input
. Notice how we can still have fine-grained control over each element, underneath Elm will then render this using a virtualdom, yes, just like what React and other frameworks does. In fact there’s benchmark where Elms implementation is the second fastest.
What about those 3rd party js libs? Elm also offers a way to communicate outside Elm and unto the js world via Ports. So you still can take advantage of the vast js ecosystem. I think its the future of writing js interfaces, one that ES6/ES2015 are still trying to solve but on a different direction.
Now go ahead and convince your boss to drop whatever it is you’re using and swith over to Elm. :)