In a previous post, I touched on how we gently introduced Elm to our production stack at NoRedInk. But we didn’t stop there! Since that first course of Elm tasted so good, we went back for seconds, then thirds, and by now elm-html and The Elm Architecture have become our preferred tools for all front-end work.
Where many languages built for the browser offer incremental improvements over JS, Elm’s core design is fundamentally different—like React and Flux taken to a whole new level. This can make parts of the language unfamiliar at first, but it enables things like a time-traveling debugger, a package system where semantic versioning is automatically enforced for every package, and large, complex applications whose Elm code has yet to throw a runtime exception.
Let’s explore these benefits by building a live-validated signup form in Elm!
Rendering a View
If you already have npm, you can get the Elm compiler by running npm install -g elm
(or alternatively you can just download the Elm installer). Afterwards you should be able to run this command and see the following output:
$ elm make --version
elm-make 0.16 (Elm Platform 0.16.0)
…followed by some help text. Make sure the Elm Platform version is 0.16.0!
Let’s start by rendering the form. Where JavaScript has Handlebars, CoffeeScript has Jade, and React has JSX, Elm also has its own declarative system for rendering elements on a page. Here’s an example:
view model =
form [ id "signup-form" ] [
h1 [] [ text "Sensational Signup Form" ],
label [ for "username-field" ] [ text "username: " ],
input [ id "username-field", type' "text", value model.username ] [],
label [ for "password"] [text "password: " ],
input [ id "password-field", type' "password", value model.password ] [],
div [ class "signup-button" ] [ text "Sign Up!" ]
]
Like any templating system, we can freely substitute in values from the model. However, unlike Handlebars, Jade, and JSX, this is not a special templating library with its own special syntax - it’s just vanilla Elm code! (You can check out the Elm Syntax Reference for more.)
The above code simply defines a function called view
with one argument (model
). Much like Ruby and CoffeeScript, everything is an expression in Elm, and the last expression in an Elm function is always used as its return value. In other words, the above code would look essentially like this in JavaScript:
function view(model) {
return form([ id("signup-form") ], [
h1([], [ text("Sensational Signup Form") ]),
label([ for("username-field") ], [ text("username: ") ]),
input([ id("username-field", type_("text"), value(model.username) ], []),
label([ for("password") ], [ text("password: ") ]),
input([ id("password-field", type_("password"), value(model.password) ], []),
div([ class("signup-button") ], [ text("Sign Up!") ])
]);
}
Notice that in Elm you call functions with spaces rather than parentheses, and omit the commas that you’d put between arguments in JS. So in JavaScript you’d write:
Math.pow(3, 7)
…whereas in Elm you’d write:
Math.pow 3 7
When using parentheses to disambiguate, you wrap the entire expression like so:
Math.pow (Math.pow 3 7) 4
On a stylistic note, the Elm Style Guide recommends putting commas at the beginning of the line:
view model =
form
[ id "signup-form" ]
[ h1 [] [ text "Sensational Signup Form" ]
, label [ for "username-field" ] [ text "username: " ]
, input [ id "username-field", type' "text", value model.username ] []
, label [ for "password" ] [ text "password: " ]
, input [ id "password-field", type' "password", value model.password ] []
, div [ class "signup-button" ] [ text "Sign Up!" ]
]
When I first started using Elm I thought this looked weird, and kept using the trailing-commas style I was accustomed to. Eventually I noticed myself spending time debugging errors that turned out to have been caused because I’d forgotten a trailing comma, and realized that with leading commas it’s blindingly obvious when you forget one. I gave this style a try, got used to it, and now no longer spend time debugging those errors. It’s definitely been worth the adjustment. Give it a shot!
Since Elm compiles to a JavaScript file, the next step is to use a bit of static HTML to kick things off. Here’s a sample page that includes a basic HTML skeleton, some CSS styling, a <script>
tag to import our compiled .js file (the Elm standard library is about the size of jQuery), and the one line of JavaScript that kicks off our Elm app:
Elm.fullscreen(Elm.SignupForm);
We’re choosing to have the Elm app use the whole page here, but we could also have it render into a single element—for example, to add a touch of Elm to an existing JavaScript code base.
Finally we need to surround our view function with some essentials that every Elm program needs: naming our module, adding relevant imports, and declaring our main function. Here’s the resulting SignupForm.elm file, with explanatory comments:
module SignupForm where
-- declares that this is the SignupForm module, which is how other modules
-- will reference this one if they want to import it and reuse its code.
-- Elm’s "import" keyword works mostly like "require" in node.js.
-- The “exposing (..)” option says that we want to bring the Html module’s contents
-- into this file’s current namespace, so that instead of writing out
-- Html.form and Html.label we can use "form" and "label" without the "Html."
import Html exposing (..)
-- This works the same way; we also want to import the entire
-- Html.Events module into the current namespace.
import Html.Events exposing (..)
-- With this import we are only bringing a few specific functions into our
-- namespace, specifically "id", "type'", "for", "value", and "class".
import Html.Attributes exposing (id, type', for, value, class)
view model =
form
[ id "signup-form" ]
[ h1 [] [ text "Sensational Signup Form" ]
, label [ for "username-field" ] [ text "username: " ]
, input [ id "username-field", type' "text", value model.username ] []
, label [ for "password" ] [ text "password: " ]
, input [ id "password-field", type' "password", value model.password ] []
, div [ class "signup-button" ] [ text "Sign Up!" ]
]
-- Take a look at this starting model we’re passing to our view function.
-- Note that in Elm syntax, we use = to separate fields from values
-- instead of : like JavaScript uses for its object literals.
main =
view { username = "", password = "" }
The starting model we’re passing to the view is:
{ username = "", password = "" }
This is a record. Records in Elm work pretty much like Objects in JavaScript, except that they’re immutable and don’t have prototypes.
Let’s build this, shall we?
$ elm make SignupForm.elm
Some new packages are needed. Here is the upgrade plan.
Install:
elm-lang/core 3.0.0
Do you approve of this plan? (y/n)
Ah! Before we can build it, first we need to install dependencies.
elm-lang/core
is a basic dependency needed by every Elm program, so we should
answer y
to install it.
Now we should see this:
Downloading elm-lang/core
Packages configured successfully!
I cannot find find module 'Html'.
Module 'SignupForm' is trying to import it.
Potential problems could be:
* Misspelled the module name
* Need to add a source directory or new dependency to elm-package.json
Ah, right. Because we’re using elm-html
in addition to core
, we’ll need
to install that explicitly:
$ elm package install evancz/elm-html
To install evancz/elm-html I would like to add the following
dependency to elm-package.json:
"evancz/elm-html": "4.0.1 <= v < 5.0.0"
May I add that to elm-package.json for you? (y/n)
The package installer wants to create an elm-package.json file for you. How nice of it! Enter y
to let it do this.
Next it will tell you it needs a few dependencies in order to install elm-html:
Some new packages are needed. Here is the upgrade plan.
Install:
evancz/elm-html 4.0.2
evancz/virtual-dom 2.1.0
Do you approve of this plan? (y/n)
Again, enter y
to install the dependencies as well as elm-html
. When it’s done, you should see this:
Packages configured successfully!
Now that we have our dependencies installed, we’re ready to build.
Some new packages are needed. Here is the upgrade plan.
$ elm make SignupForm.elm
After this completes, you should see:
Successfully generated index.html.
Sure enough, the current folder will now have an index.html file which contains your Elm code compiled to inline JavaScript. By default, elm make
generates this complete
.html file so that you can get something up and running with minimal fuss, but
we’ll compile it to a separate .js file in a moment.
You should also now have an elm-stuff/
folder, which contains cached build data and your downloaded packages - similar to the node_modules/
folder for your npm
packages. If you’re using version control and like to put node_modules
in your ignore list, you’ll want elm-stuff/
in your ignore list as well.
If you open up index.html
in a browser, you’ll see an unstyled, fairly
disheveled-looking signup form. Let’s improve on that by splitting out the
JavaScript so we can put it in some nicer surroundings.
elm make SignupForm.elm --output elm.js
Success! Compiled 0 modules.
Successfully generated elm.js
The --output
option specifies the file (either .html or .js) that will
contain your compiled Elm code.
See how it says Compiled 0 modules
? This is because elm make
only
bothers rebuilding parts of your code base that changed, and since nothing
changed since your last build, it knows nothing needed to be rebuilt. All
it did was output the results of its previous build into elm.js.
Download the HTML boilerplate snippet as example.html, put it in the same directory as your elm.js file, and open it. Since example.html loads a file called elm.js, you should now see a signup form like so:
![signup-1]()
Congratulations! You’ve now built a simple static Elm view and rendered it in the browser.
Next we’ll add some interaction.
Expanding the Model
Let’s add a touch of interactivity: when the user submits an empty form, display appropriate error messages.
First, we’ll introduce some validation error messages to our model, and render them in the view:
view model =
form
[ id "signup-form" ]
[ h1 [] [ text "Sensational Signup Form" ]
, label [ for "username-field" ] [ text "username: " ]
, input [ id "username-field", type' "text", value model.username ] []
, div [ class "validation-error" ] [ text model.errors.username ]
, label [ for "password" ] [ text "password: " ]
, input [ id "password-field", type' "password", value model.password ] []
, div [ class "validation-error" ] [ text model.errors.password ]
, div [ class "signup-button" ] [ text "Sign Up!" ]
]
If we wrote this same logic in JavaScript, on page load we’d get a runtime exception: “cannot read value "username" of undefined.
” Oops! We’re trying to access model.errors.username
in our view function, but we forgot to alter the initial model in our main
function to include an errors
field.
Fortunately, Elm’s compiler is on top of things. It will actually figure out that we’ve made this mistake and give us an error at build time, before this error can reach our users!
$ elm make SignupForm.elm --output elm.js
==================================== ERRORS ====================================
-- TYPE MISMATCH ------------------------------------------------ SignupForm.elm
The argument to function `view` is causing a mismatch.
39│ view { username = "", password = "" }
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Function `view` is expecting the argument to be:
{ b | ..., errors : ... }
But it is:
{ ... }
Detected errors in 1 module.
Let’s take a closer look at what it’s telling us.
Function `view` is expecting the argument to be:
{ b | ..., errors : ... }
But it is:
{ ... }
This tells us that our view
function is expecting a record with a field called errors
(among other fields - hence the ...
), but the record we’ve provided does not have an errors
field! Sure enough, we totally forgot to include that. Whoops.
The compiler knows this is broken because we pass that record into our view
function as the model argument, and the view
function then references model.errors.username
and model.errors.password
- but given the code we’ve written, there’s no possible way model.errors
could be defined at that point.
This is a classic example of the Elm compiler saving us from a runtime exception. It’s really, really good at this - so good that Dreamwriter’s entire Elm codebase still hasn’t thrown a single runtime exception!
Now that we know about the problem, we can fix it by adding an errors
field like so:
initialErrors =
{ username = "bad username", password = "bad password" }
main =
view { username = "", password = "", errors = initialErrors }
Let’s rebuild with elm make SignupForm.elm --output elm.js
(which should succeed now) and open the browser. We should see this:
![signup-2]()
Incidentally, in a larger project we’d probably use a build tool plugin like grunt-elm or gulp-elm or elm-webpack-loader rather than directly running elm make
every time, but for this example we’ll keep things simple.
Before we move on, let’s make initialErrors
start out empty, as the user should not see “bad username” before having had a chance to enter one:
initialErrors =
{ username = "", password = "" }
Next we’ll update the model when the user interacts.
Adding Validation Logic
Now that we’re rendering errors from the model, the next step is to write a validation function that populates those errors. We’ll add it right before our main function:
getErrors model =
{ username =
if model.username == "" then
"Please enter a username!"
else
""
, password =
if model.password == "" then
"Please enter a password!"
else
""
}
For comparison, here’s how this would look in (similarly formatted) JavaScript:
function getErrors(model) {
return {
username:
(model.username === ""
? "Please enter a username!"
: ""),
password:
(model.password === ""
? "Please enter a password!"
: "")
};
}
To recap: we have a starting model, a validation function that can translate that model into error strings, and a view that can report those errors to the user. The next step is to wire it all together. When the user clicks Sign Up, we should use all the pieces we’ve built to display any appropriate errors.
How do we do that? There’s a convenient library called StartApp that makes it easy. Install it like so:
$ elm package install evancz/start-app
We’re also going to want Elm Effects later on, so let’s go ahead and grab that too while we’re at it:
$ elm package install evancz/elm-effects
Next let’s import some modules from these new packages at the top of SignupForm.elm, right below the module declaration, like so:
import StartApp
import Effects
StartApp wraps some boilerplate wiring that most Elm applications (following the The Elm Architecture) will use. Let’s replace our main
implementation with code that uses StartApp instead:
app =
StartApp.start
{ init = ( initialModel, Effects.none )
, update = update
, view = view
, inputs = []
}
main =
app.html
StartApp.start
wires everything up and returns a record we’ll name app
, and from there app.html
conveniently provides everything we need to implement main
.
Now that that’s out of the way, we need to provide StartApp with an update function to update our model. Let’s add it below our getErrors
function:
update action model =
if action.actionType == "VALIDATE" then
( { model | errors = getErrors model }, Effects.none )
else
( model, Effects.none )
This update
function’s job is to take an Action (a value that describes what we want done) and a model, and return two things:
- A new model, with any relevant updates applied
- A description of any effects we want done - such as firing AJAX requests or kicking off animations. We’ll dive into using effects later, but for now we’ll stick to no effects:
Effects.none
Have a look at what the else
branch returns:
( model, Effects.none )
Whenever you see a comma-separated list of elements inside parentheses like this, what you’re seeing is a tuple. A tuple is like a record where you don’t bother naming the fields; ( model, Effects.none )
is essentially a variation on { model = model, effects = Effects.none }
where fields are accessed by position rather than by name.
Here the else
branch returns a tuple containing the original model unchanged, as well as Effects.none
- indicating we do not want any effects like AJAX requests fired off. Compare that to the if
branch:
if action.actionType == "VALIDATE" then
( { model | errors = getErrors model }, Effects.none )
This says that if our Action record has an actionType
field equal to "VALIDATE"
then we should return an altered model and (once again) Effects.none
. The first element in this tuple is an example of a record update:
{ model | errors = getErrors model }
You can read this as “Return a version of model
where the errors
field has been replaced by the result of a call to getErrors model
.” (Remember that records are immutable, so an “update” doesn’t actually change them; rather, an “update” refers to returning a separate record which has had the update applied.)
Since our view is already displaying the result of model.errors
, and getErrors
returns an appropriate list of errors, all that’s needed to perform the validation is to pass this update
function an Action record like { actionType = "VALIDATE", payload = "" }
and we’ll end up displaying the new errors!
Now we’re ready to connect what we’ve done to a click handler to our submit button. We’ll change it from this:
, div [ class "signup-button" ] [ text "Sign Up!" ]
…to this:
, div [ class "signup-button", onClick actionDispatcher { actionType = "VALIDATE", payload = "" } ] [ text "Sign Up!" ]
onClick
takes two arguments: an Action - in this case the plain old record { actionType = "VALIDATE", payload = "" }
- and somewhere useful to send that action - in this case actionDispatcher
. When the user clicks this button, the record { actionType = "VALIDATE", payload = "" }
will be sent to actionDispatcher
, which is what StartApp uses to connect our view
function to our update
function. Since StartApp will be passing this dispatcher to our view
function, we need to modify view
to accept it as an argument:
view actionDispatcher model =
Finally, we need to delegate our main logic to StartApp, which we can do like so:
initialModel = { username = "", password = "", errors = initialErrors }
app =
StartApp.start
{ init = ( initialModel, Effects.none )
, update = update
, view = view
, inputs = []
}
main =
app.html
Let’s build and try to use what we’ve put together so far. Alas and alack! As soon as we submit the form, it clears out everything we’ve typed and presents us with validation errors. What gives?
This is because when we submit the form, it re-renders everything based on the values in the current model…and we never actually updated those! No matter how much you type into either the username field or the password field, those values will not make it into the model of their own free will.
We can fix that using the same technique as before. First we’ll add to our update
function:
update action model =
if action.actionType == "VALIDATE" then
( { model | errors = getErrors model }, Effects.none )
else if action.actionType == "SET_USERNAME" then
( { model | username = action.payload }, Effects.none )
else if action.actionType == "SET_PASSWORD" then
( { model | password = action.payload }, Effects.none )
else
( model, Effects.none )
Now if this function receives something like { actionType = "SET_USERNAME", payload = "rtfeldman" }
, it will update the model’s username
field accordingly.
Let’s create an on
handler with the input
event (since input
events fire not only on keyup, but also whenever a text input otherwise changes - e.g. when you right-click and select Cut), which will dispatch one of these new Actions:
, input
[ id "username-field"
, type' "text"
, value model.username
, on "input" targetValue (\str -> Signal.message actionDispatcher { actionType = "SET_USERNAME", payload = str })
]
[]
There are two new things here.
The first is targetValue
, which is an example of a Decoder. A Decoder’s job is to translate a JSON string or raw JavaScript value (in this case the gargantuan Event
object) into something simpler and more useful. We can create our own Decoders if we like, but in this case an off-the-shelf one does what we need; targetValue
is a Decoder that performs the equivalent of calling event.target.value
in JavaScript - in other words, it gets you the text contents of the input field.
The other thing is (\str -> Signal.message…)
- this is just Elm syntax for an anonymous function. The JavaScript equivalent of this would be:
on("input", getTargetValue, function(str) { return Signal.message(actionDispatcher, { actionType = "SET_USERNAME", payload = str }); });
Finally let’s give password
the same treatment:
view actionDispatcher model =
form
[ id "signup-form" ]
[ h1 [] [ text "Sensational Signup Form" ]
, label [ for "username-field" ] [ text "username: " ]
, input
[ id "username-field"
, type' "text"
, value model.username
, on "input" targetValue (\str -> Signal.message actionDispatcher { actionType = "SET_USERNAME", payload = str })
]
[]
, div [ class "validation-error" ] [ text model.errors.username ]
, label [ for "password" ] [ text "password: " ]
, input
[ id "password-field"
, type' "password"
, value model.password
, on "input" targetValue (\str -> Signal.message actionDispatcher { actionType = "SET_PASSWORD", payload = str })
]
[]
, div [ class "validation-error" ] [ text model.errors.password ]
, div [ class "signup-button", onClick actionDispatcher { actionType = "VALIDATE", payload = "" } ] [ text "Sign Up!" ]
]
Now whenever the user types in the username
field, an Action is created (with its actionType
field being "SET_USERNAME"
and its payload
field being the text the user entered. Remember that Actions are just data; they don’t intrinsically do anything, but rather describe a particular model change so that other logic can actually implement that change.
Try it out! Everything should now work the way we want.
One great benefit of this architecture is that there is an extremely obvious (and guaranteed!) separation between model and view. You never have model updates sneaking into your view, causing hard-to-find bugs, because views aren’t even capable of updating models. Each step of the process is a straightforward transformation from one sort of value to the next: inputs to actions to model to view.
It may not be what you’re used to, but once you get in the Elm groove, debugging and refactoring get way easier. Based on my experience writing production Elm at NoRedInk, I can confirm that these things stay easier as your code base scales.
Asking a server about username availability
Finally we’re going to check whether the given username is available. Rather than setting up our own server, we’re just going to use GitHub’s API; if GitHub says that username is taken, then that’s our answer!
To do this, we’ll need to use the elm-http library to communicate via AJAX to GitHub’s API. Let’s start by installing it:
elm package install evancz/elm-http
Up to this point we haven’t written any code that deals with effects. All of our functions have been stateless; when they are given the same inputs, they return the same value, and have no side effects.
Now we’re about to…continue that trend! Not only does Elm represent Actions as data, it represents effects as data too—specifically, using values called Tasks. Tasks have some things in common with both Promises and callbacks from JavaScript. Like Promises, they can be easily chained together and have consistent ways of representing success and failure. Like callbacks, instantiating Tasks doesn’t do anything right away; you can instantiate a hundred Tasks if you like, and nothing will happen until you hand them off to something that can actually run them. Like both Promises and callbacks, Tasks can represent either synchronous or asynchronous effects.
The Elm Effects library lets us run Tasks in a way that always results in an Action being fed back into our update function, which means we can neatly incorporate effects into the architecture we’ve been following this entire time. The only difference will be that sometimes instead of returning Effects.none
in our tuple, we’ll return Effects.task
instead.
Let’s update our "VALIDATE"
action to fire off a request to GitHub’s API. In normal Elm code we wouldn’t be this verbose about it, but for learning purposes we’ll break everything down into small pieces.
if action.actionType == "VALIDATE" then
let
url =
"https://api.github.com/users/" ++ model.username
usernameTakenAction =
{ actionType = "USERNAME_TAKEN", payload = "" }
usernameAvailableAction =
{ actionType = "USERNAME_AVAILABLE", payload = "" }
request =
Http.get (succeed usernameTakenAction) url
neverFailingRequest =
Task.onError request (\err -> Task.succeed usernameAvailableAction)
in
({ model | errors = getErrors model }, Effects.task neverFailingRequest)
That’s a decent bit of new stuff, so let’s unpack it!
First, the conditional is now using a let
expression. These allow you to make local declarations that are scoped only to the let
expression; the rest of our program can’t see usernameAvailableAction
or request, but anything between let
and the expression just after in
can see them just fine. The whole let
expression ultimately evaluates to the single expression after in
.
Since url
, usernameTakenAction
, and usernameAvailableAction
are all things we’ve seen before, let’s look at Http.get
next. It takes two arguments: a Decoder and a URL string. The URL is what you’d expect, and the decoder is like targetValue
from our input event handler earlier: it describes a way to translate an arbitrary JavaScript value into something more useful to us.
In this case, we’re not using a very interesting Decoder because any API response error means the username was not found (and thus is available), and any success means the username is taken. Since the Decoder only gets used if the API response came back green, we can use the succeed
Decoder, which always decodes to whatever value you give it. (succeed usernameTakenAction
means “don’t actually decode anything; just always produce usernameTakenAction
no matter what.”)
Finally we have neverFailingRequest
. This is important, because the way the Effects library is able to consistently translate a Task into an Action is by requiring that any errors be handled up front. Like JavaScript Promises, Elm Tasks can either succeed or fail, and Http.get
returns a Task that can most definitely fail (with a network error, 404 Not Found, and so on). Since Effects.task
requires a Task that can never fail, we need to handle that failure case somehow.
Fortunately, the Task.onError
function lets us translate any task failure into a success - in this case, a success where we produce usernameAvailableAction
to report that the username is available. Since every failure is now translated into a success, we can give Effects.task
what it requires: a Task that can never fail. We’re all set!
Now when we enter a username, if that username is available on GitHub (meaning the Http.get
results in a 404 Not Found error and thus a failed Task), the result is { actionType = "USERNAME_AVAILABLE", payload = "" }
being passed to our update
function. If the username is taken (meaning the Http.get
resulted in a successful Task), the result is that our “always results in usernameTakenTask
” Decoder causes { actionType = "USERNAME_TAKEN", payload = "" }
to be passed to our update
function instead.
Let’s incorporate these new actions into our update
function:
update action model =
...
else if action.actionType == "USERNAME_TAKEN" then
( withUsernameTaken True model, Effects.none )
else if action.actionType == "USERNAME_AVAILABLE" then
( withUsernameTaken False model, Effects.none )
else
( model, Effects.none )
withUsernameTaken isTaken model =
let
currentErrors =
model.errors
newErrors =
{ currentErrors | usernameTaken = isTaken }
in
{ model | errors = newErrors }
Before we can use all these fancy new libraries, we’ll need to import them at the top, once again below our module declaration:
import Http
import Task exposing (Task)
import Json.Decode exposing (succeed)
Remember how Tasks don’t actually do anything until you hand them off to something that can run them? StartApp takes care of packaging up the Tasks that come out of our update
function into something that’s easily runnable (and exposes it as app.tasks
), but to actually run them we need to add this to the end of our file:
port tasks : Signal (Task Effects.Never ())
port tasks =
app.tasks
Ports are a subject for another post, but for now it’s enough to know that this will get us what we want. Finally, we need to introduce the “username taken” validation error to initialErrors
, getErrors
, and view
:
initialErrors =
{ username = "", password = "", usernameTaken = False }
getErrors model =
…
, usernameTaken = model.errors.usernameTaken
}
view actionDispatcher model =
…
, div [ class "validation-error" ] [ text (viewUsernameErrors model) ]
viewUsernameErrors model =
if model.errors.usernameTaken then
"That username is taken!"
else
model.errors.username
With the comments removed, here’s our final result.
Now the username
field is validated for availability, and everything else still works as normal. We did it!
Wrapping Up
We’ve just built a live-validated signup form with the following characteristics:
- Clear, obvious, and strict separation between model and view
- A compiler that told us about type errors before they could become runtime exceptions
- High-performance reactive DOM updates like React and Flux
- Live AJAX validation of whether a username is available
- Immutability everywhere
- Not a single function with a side effect
To save time, we didn’t do everything perfectly idiomatically. The fully idiomatic approach would have been to split this up into several files, to represent Actions using the more concise and robust union types and case
expressions instead of records and if
/then
/else
expressions, and to document various functions using type annotations.
I plan to do a follow-up post explaining how to get from this code to the idiomatic version, and expanding on this example to show techniques like:
- Using the contents of AJAX responses in business logic
- Using fields with different types of inputs - checkboxes, dropdowns, etc.
- Integrating a third-party JavaScript library (mailcheck) for suggesting misspelled email addresses
- Submitting the form two ways: either using AJAX or using direct form submission
- Sending anti-CSRF authenticity tokens along with AJAX requests
Stay tuned!
By the way, if working on Elm code in production appeals to you…we’re hiring!
![]()
Richard Feldman
@rtfeldman
Engineer at NoRedInk
Thanks to Joel Clermont, Johnny Everson, Amjith Ramanujam, Jonatan Granqvist, Brian Hicks, Mark Volkmann, and Michael Glass for reading drafts of this.