Taking Elm for a Test Drive

Andrew Hao ·

Elm emerged on the scene in early 2012 as a strongly-typed, functional language that compiles down to Javascript. With its architecture and type system, it claims to provide bulletproof guardrails to help developers build systems that are highly reliable, with “no runtime exceptions in practice”.

Elm prides itself on having a low barrier of entry – it can be introduced as a component into an existing web app, so long as your app can provide it a self-contained div. In fact, the creators of Elm strongly advocate taking an incremental approach to introducing Elm into your systems.

Lately, a few Carbon Fivers and I have been taking the language out for a spin and discovering what it means to write software systems in Elm. In this post, we’ll walk through what it looks like to take a small form widget written in vanilla jQuery and convert it to Elm, picking up language basics and learning to write apps the Elm way. We’ll also discuss the unique feature set that makes Elm apps so reliable.

For the sake of this example, we assume that you’ve familiarized yourself with the tutorials in the official Elm Guide and have a grasp of Elm language basics. Let’s try to refactor one component in our current web app and watch this transformation play out!

Form Validation, The Old Way

In our old world, we used vanilla jQuery to render an HTML component, running validations on the fields as users typed. Notably, the form widget would do the following behaviors:

  1. Render an HTML form
  2. Validate the presence of user-provided input
  3. Validate the format of various fields through regex matching
  4. Display error messages if any validation errors existed
  5. Prevent form submission if any validation errors existed

Let’s look at how this might have been coded up:


class ValidatedForm {
constructor(parentEl) {
this.parentEl = parentEl;
this.isValid = true;
}
render() {
const formEl = $(`
<form>
<div class="form-group">
<input type="text" name="name" placeholder="Name" />
<div class="form-group-validation"></div>
</div>
<div class="form-group">
<input type="phone" name="phone" placeholder="Telephone" />
<div class="form-group-validation"></div>
</div>
<button type="submit">Submit</button>
</form>
`);
this.parentEl.html(formEl);
this.formEl = formEl;
this.nameEl = formEl.find("[name='name']");
this.phoneEl = formEl.find("[name='phone']");
this._bind();
return this.parentEl;
}
_bind() {
this._bindInputHandlers();
this._bindFormSubmit();
}
_bindInputHandlers() {
this.nameEl.on("input", () => this._validate());
this.phoneEl.on("input", () => this._validate());
}
_validate() {
const isNameValid = this._validateNameField(this.nameEl.val());
const isPhoneValid = this._validatePhoneField(this.phoneEl.val());
this._renderNameValidationMessage(isNameValid);
this._renderPhoneValidationMessage(isPhoneValid);
const isAllValid = isNameValid && isPhoneValid;
this._disableSubmitButton(isAllValid);
return isAllValid;
}
_disableSubmitButton(isValid) {
this.parentEl.find("button").prop("disabled", !isValid);
}
_renderPhoneValidationMessage(isValid) {
this.phoneEl.toggleClass("has-error", !isValid);
this.phoneEl
.parent()
.find(".form-group-validation")
.text(isValid ? "" : "Phone number invalid");
}
_renderNameValidationMessage(isValid) {
this.nameEl.toggleClass("has-error", !isValid);
this.nameEl
.parent()
.find(".form-group-validation")
.text(isValid ? "" : "Name invalid");
}
_validateNameField(value) {
return value != null && value !== "";
}
_validatePhoneField(value) {
return (
value.match(
/^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]{0,1}\d{3}[\s.-]{0,1}\d{4}$/
) !== null
);
}
_bindFormSubmit() {
this.formEl.on("submit", e => {
if (!this.isValid) {
e.preventDefault();
e.stopPropagation();
}
});
}
}

This is a fairly straightforward jQuery implementation of form validation logic – a couple of DOM event handlers that bind to user input, some state stored in a JS object, and some rendering logic responding to the validation state.

However, there are some weaknesses to this approach:

  • There is fragile code that is very order-dependent based on the state of the DOM nodes. For example, we must remember to always attach event handlers each time the form is rendered.
  • There is implicit state stored in the DOM that must be carefully hooked up to the validation logic that renders validation messages. This data binding must be carefully implemented.
  • Javascript does not provide any type safety guarantees, and its dynamic nature means that we leave ourselves open to any runtime errors should we accidentally mistype a variable name, or accidentally pass a `null`.

Now, the Javascript world has many solutions to these problems. Ember and Angular introduced us to the power of automagic data binding. React took this idea further and enforced one-way data flow to the views. Flux and Redux introduced the idea of modeling state as a tree and side effects as pure functions.

Elm takes many of these ideas and wraps it up in a typed functional language and an elegant application architecture. When paired with its helpful compiler, writing frontend applications begins to become a surprisingly smooth affair.

Rewriting it in Elm

Let’s begin by exploring what replacing this widget with an Elm app would look like. A basic Elm app is made up of the model, the view, and the update functions. Let’s dive into the model:

Model

The model might be thought of the state tree – this is the general state that must be stored with the application.


type alias Model =
{ name : String
, phone : String
}
model : Model
model =
{ name = ""
, phone = ""
}

Here, we define two bits of state our app will need – the user’s input of the name and the phone number.

Update

The update function contains functions that apply changes to the model state based on update messages.

First, we define the update messages in the app. These can be thought of commands, or events that are triggered by the user, or by outside events in the system.


type Msg
= UpdateName String
| UpdatePhone String

Here, we model two messages that occur in the app – each one is fired when a user types into the specific form field. We’ll see how they get fired when we get to our discussion on the view. A UpdateName String can be read to mean “The UpdateName message type that takes in a String as one of its arguments – in this case, it’s the String that corresponds to the value in the text field.”

We then define an update function that handles each of these messages:


update : Msg -> Model -> Model
update msg model =
case msg of
UpdateName newName ->
{ model | name = newName }
UpdatePhone newPhone ->
{ model | phone = newPhone }

The update function is one large case statement that modifies state on the Model. The functions simply update the corresponding field in the model‘s state record, then return the updated model to the function caller.

Redux programmers here may find this pattern quite familiar – in fact, Redux’s reducers were inspired by Elm’s update architecture.

View

Finally, the view function’s job is to render the DOM based on the data in the model.


view : Model -> Html Msg
view model =
Html.form [ action "/path/to/api" ]
[ div [ class "form-group" ]
[ input
[ placeholder "Please enter something"
, onInput UpdateName
, value model.name
, name "name"
]
[]
, div [] [ model.name |> (hasNameValidationError >> nameErrorMessage) |> text ]
]
, div [ class "form-group" ]
[ input
[ placeholder "Please enter something"
, onInput UpdatePhone
, value model.phone
, name "tel"
]
[]
, div [] [ model.phone |> (hasPhoneValidationError >> phoneErrorMessage) |> text ]
]
, button [ disabled (hasAnyError model) ] [ text "Submit" ]
]

The most obvious observation about the view is that it uses functions to model an HTML DOM tree. Here, the DOM tree is modeled as the nested composition of functions that correspond to DOM nodes (div, input, button). The view takes in app state (Model) and renders the DOM with a Html Msg, which Elm’s highly optimized DOM rendering engine uses to render out to the browser.

The view also introduces event handlers for user input to enter back into the app – the onInput handlers will fire UpdatePhone or UpdateNamemessages, which will be piped by the runtime back into the update function.

Elm takes React’s ideas of functional-style unidirectional data flow and cranks it up to eleven. Because the view is a pure function, we are guaranteed that data flow goes in a single direction from state to the DOM. With the compiler’s type safety guarantees, the developer cannot accidentally introduce side effects in functions like the view. It’s simply impossible to do so in the language.

Putting it all together

Here’s the entire app. Note that we’ve included a few more helper methods that compute error messages and enable or disable the form based on validation outcomes.


module ValidatedForm exposing (..)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Regex exposing (contains, regex)
main : Program Never Model Msg
main =
Html.beginnerProgram { model = model, view = view, update = update }
— MODEL
type alias Model =
{ name : String
, phone : String
}
model : Model
model =
{ name = ""
, phone = ""
}
— UPDATE
type Msg
= UpdateName String
| UpdatePhone String
update : Msg -> Model -> Model
update msg model =
case msg of
UpdateName newName ->
{ model | name = newName }
UpdatePhone newPhone ->
{ model | phone = newPhone }
— VIEW
hasNameValidationError : String -> Bool
hasNameValidationError nameEntry =
nameEntry == ""
hasPhoneValidationError : String -> Bool
hasPhoneValidationError phoneEntry =
let
phoneRegex =
"^(\\+\\d{1,2}\\s)?\\(?\\d{3}\\)?[\\s.-]{0,1}\\d{3}[\\s.-]{0,1}\\d{4}$"
in
not <| contains (regex phoneRegex) phoneEntry
nameErrorMessage : Bool -> String
nameErrorMessage hasNameError =
if hasNameError then
"Name must be supplied"
else
""
hasAnyError : Model -> Bool
hasAnyError model =
(hasNameValidationError model.name) || (hasPhoneValidationError model.phone)
phoneErrorMessage : Bool -> String
phoneErrorMessage hasPhoneError =
if hasPhoneError then
"Phone must match format 555-555-1234"
else
""
view : Model -> Html Msg
view model =
Html.form [ action "/path/to/api" ]
[ div [ class "form-group" ]
[ input
[ placeholder "Please enter something"
, onInput UpdateName
, value model.name
, name "name"
]
[]
, div [] [ model.name |> (hasNameValidationError >> nameErrorMessage) |> text ]
]
, div [ class "form-group" ]
[ input
[ placeholder "Please enter something"
, onInput UpdatePhone
, value model.phone
, name "tel"
]
[]
, div [] [ model.phone |> (hasPhoneValidationError >> phoneErrorMessage) |> text ]
]
, button [ disabled (hasAnyError model) ] [ text "Submit" ]
]

See this application run on the Ellie web editor.

There we have it! We’ve built a little Elm program.

This style of building apps with the model-update-view architecture is dubbed The Elm Architecture (shortened to TEA). It’s particularly well-suited for functional languages, as everything can be modeled by pure functions, solving a class of application errors and bugs that arise when attempting to manage state in complex apps.

Incrementally Growing Your Systems with Elm

While our coworkers like the concept of Elm, they’re wary of its claims and don’t want to sink time and money rewriting the entire core app. And they would be right! What if we took an incremental approach? In our team’s experience, the form validation widget has been a complicated, buggy mess. If we can just demonstrate that writing Elm is easily integrated into our existing ecosystem and that it results in increased reliability and maintainability, we just might be able to make our case stronger.

Since Elm is easily embeddable into small apps – let’s demonstrate a sample ES6 wrapper around the compiled Elm app:


class ValidatedForm {
constructor(parentEl) {
this.parentEl = parentEl;
}
render() {
Elm.ValidatedForm.embed(this.parentEl)
}
}

This embeds the Elm app and instantiates it in the root div that the prior widget used to inhabit. That was easy!

 

Is Elm for me? Is it for my company?

Elm as a language is young and the community is in its infancy. While it’s not as widespread a technology as other established JS frameworks, it’s rising in adoption across the industry. It’s certainly made waves in the development world that has us keeping our eyes on the language in the future.

By introducing a way to refactor a Javascript widget to an Elm component, we can start seeing the benefits of using this friendly, reliable programming language. Due to its ease of integration with native Javascript, it has a compelling story for gradual introduction into your frontend systems. A gradual approach also allows your organization to evaluate its merits at a controlled scale and determine whether the benefits of the language and runtime outweigh the cost of introducing another new technology.

What do you think? Did you find yourself getting used to the compiler? Was thinking in functions and function composition interesting to you? Did you start to see the power of using types and the type system? Let us know on Twitter!

Andrew Hao
Andrew Hao

Andrew is a design-minded developer who loves making applications that matter.