Heading image for post: Elm by Example: Soup to Nuts

Javascript

Elm by Example: Soup to Nuts

Profile picture of Dorian Karter

I've been experimenting with Elm for the past few months and have come to really appreciate its style of programming. It is very similar to React in the sense that you can render modular components based on DOM events, but the functional style and syntactic sugar are a pleasure to work with. In this blog post I will guide you in building your first Slack inspired component.

By now you have probably heard about Elm, the statically typed, immutable, Haskell inspired, polite and helpful, functional reactive language for the web.

It's extremely FAST too. It consistently performs better than React, Ember, Angular and others in the TODO MVC performance tests.

performance comparison

I've been experimenting with Elm for the past few months and have come to really appreciate its style of programming. It is very similar to React in the sense that you can render modular components based on DOM events, but the functional style and syntactic sugar are a pleasure to work with.

My favorite thing about Elm is that it is statically typed, yet type inferred. What that means is you can prototype quickly, and don't have to use type annotations, but the compiler will infer the types for you by flowing through your code and failing to compile when you did something wrong. This gives rise to Elm's best feature:

NO RUNTIME EXCEPTIONS!

This is a really big deal! After having written a fair amount of Elm it almost feels irresponsible writing JavaScript without this feature. Elm accomplishes this by forcing you to handle values that can be null before allowing you to compile your project. It also makes sure that you handle all potential values when using conditionals/pattern matching.

As you work with Elm its awesomeness unfolds before you, and you will learn interesting Computer Science concepts, particularly if you have never worked with Haskell or other functional languages. Although Haskell can be hard to learn, Elm is very pragmatic and approachable and can be used to replace both standalone JavaScript libraries and rich UI components. Elm also lends itself really well for game programming due to its rich HTML5 Canvas abstraction and input interaction using signals.

Motivation

My reason for writing this blog post is that I was struggling with some of the more advanced concepts of Elm, namely Signals, Mailboxes, and Ports. I started writing a post about how to roll out your own Model View Update pattern in Elm without the StartApp but it was hard to start without an initial example, so I decided to write this post first to lead into the next one.

In this two-part blog post I will take you through building your first Elm component - a Slack inspired quick channel switcher (Cmd+k).

I chose this component because it was small, practical, and combined multiple Signals, namely HTML Signals and Keyboard Signals, making it an ideal candidate for introducing Signals and Mailboxes.

Slack channel switcher

Prerequisites

I'm assuming basic familiarity with the Elm syntax. If you are not familiar with the Elm syntax see the official syntax documentation.

Consider the above to be Part 0.

Getting Elm

To get started you will need to install Elm on your machine.

npm update && npm install -g elm

This article is written for Elm v0.16

You can also download the .pkg installer from the elm-lang.org website.

You will also need a syntax highlighter for your editor. Here's the one I use for Vim: https://github.com/ElmCast/elm-vim

Installing required packages

Elm comes with an especially "polite" and quite "intelligent" package manager. It takes a Github relative url as an argument.

Create a project directory and cd into it.

Install the following packages:

elm package install evancz/elm-html
elm package install evancz/start-app
elm package install circuithub/elm-html-extra

Bootstrapping the component

In your favorite code editor, create a ChannelSwitcher.elm file.

First we need to declare the component, this is done with one line in elm which should be at the top of your file.

module ChannelSwitcher where

Now, below that, we need to import all the necessary modules:

-- IMPORTS
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Html.Events.Extra exposing (..)
import String
import StartApp.Simple as StartApp

Note that I'm using exposing (..) on some of the imports, the .. exposes all public functions from that module into the current scope so you can call them without prefixing with the module name.

It is usually a best practice to avoid this type of exposure as much as possible to prevent naming collisions and ambiguity. However in this case it provides convenience when writing HTML.

MODEL VIEW UPDATE

Elm uses a Model View Update architecture which dictates the way data flows through an Elm application. You can think of an Elm application as a stream of events which are converted into actions, which then calculate the new state and render HTML.

I like annotating my code with sections so that it is organized and I know where to look for things so I label it like so:

module ChannelSwitcher where

-- IMPORTS
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Signal
import StartApp.Simple as StartApp

-- MODEL

-- UPDATE

-- VIEW

Writing the HTML in Elm

Now that we imported all of the HTML attributes it's time to write some Elm-flavored HTML.

Under the view section declare a view function, follow that function with a main function, the entry point for any component. For now we will just use it to call view so we can see the HTML we generated.

-- VIEW
view =
  div [ class "container" ] [
    input [ class "search-box" ] [ ],
    ul [ class "collection" ] [
        li [ class "collection-item active" ] [ text "#Elm" ],
        li [ class "collection-item" ] [ text "#react.js" ],
        li [ class "collection-item" ] [ text "#ember" ]
    ]
  ]

main =
    view

If you are familiar with React the code above should seem familiar. Think of it as the render function in react.

The code above should be pretty self explanatory, the first square bracket of each element is its attributes, and the second is the tag content.

The text function and all other HTML elements (div, input, ul, li) are exposed and available to us by exposing the Html module. The class function used in the square brackets is imported from Html.Attributes.

It is important at this point to think of the markup representing html elements as functions, because they are. When you call Html.div [] [] in the repl you would get an Elm record representing the DOM element as data. It will look something like this:

{ type = "node", tag = "div", facts = {}, children = {}, namespace = <internal structure>, descendantsCount = 0 }

This is important because you cannot have two adjacent elements (e.g. <h1></h1><h2></h2>) without a top level wrapping element (e.g. <div><h1></h1><h2></h2></div>) and thinking of h1 and h2 as functions you quickly realize there is no nice way to return them as a function result in an immutable programming language without wrapping them with a third function.

Compiling and Running in your browser

Now we are ready to compile and run our application. In the terminal run elm make ChannelSwitcher.elm. This will generate an index.html for you, go ahead and open that in your browser.

You should be able to see the following interface:

First Step

Defining the Model

The data that we need to flow through our component for it to generate the correct output should represent a list of channels.

We start by defining a record, and we will also define an initial model so we have something to work with.

-- MODEL
type alias Model =
  { channels: List String
  , selectedChannel: Int
  , query: String
  }

initialModel : Model
initialModel =
  { channels = ["Elm", "React.js", "Ember", "Angular 2", "Om", "OffTopic" ]
  , selectedChannel = -1
  , query = ""
  }

You don't have to call your type Model it can be anything.

Defining Actions and the Update function

As the user is interacting with the component, a new "state" of the model will be calculated. For example, selectedChannel will start at -1 and as we press the arrow keys up and down it will change to 0, 1, 2 etc.. Elm creates that new model using the update function. Our update function will take an action and return a new version of the model with a small modification.

This makes it very convenient since you can look at the action type definition (see below) and immediately know what kind of transformations can happen to the model in this component.

-- UPDATE
type Action = NoOp | Filter String | Select Int

update action model =
  case action of
    NoOp ->
      model

    Filter query ->
      { model | query = query }

    Select index ->
      { model | selectedChannel = index }

The Action type we defined is a Union Type which allows us to perform Pattern Matching in the update function with the case statement.

The Filter String part is basically a Tagged Union where Filter is the action tag with a String argument. This helps us differentiate it from other actions that may have a one string argument.

Putting it all together with StartApp

It's time to put it all together using StartApp. StartApp lets us declare which methods correspond with our model, update and view parts of our component.

Replace the main function with the following:

main : Signal Html
main =
  StartApp.start
    { model = initialModel
    , update = update
    , view = view
    }

If you try and compile the code so far you will get the following message:

==================================== ERRORS ====================================

-- TYPE MISMATCH ------------------------------------------- ChannelSwitcher.elm

The argument to function `start` is causing a mismatch.

51│   StartApp.start
52│>    { model = initialModel
53│>    , update = update
54│>    , view = view
55│>    }

Function `start` is expecting the argument to be:

    { ..., view : Signal.Address Action -> Model -> Html }

But it is:

    { ..., view : VirtualDom.Node }

Detected errors in 1 module.

That's because StartApp is passing a Mailbox address and the currently computed model to the view. Don't worry about understanding Mailboxes just yet, we will cover those in the second part of the tutorial. For now, to fix this error let's refactor our view function signature to the following, and add a type annotation while we are at it:

view : Signal.Address Action -> Model -> Html
view address model =

Now you should be able to compile but as you notice when you open index.html the component still does not filter the list. Next we will render the model and implement the search/filter functionality.

Rendering the model

Let's render the model now instead of static data. Under the VIEW section we will add a new method that renders the li elements using the channels list on the model.

-- VIEW
renderChannel : String -> Html
renderChannel name =
  li [ class "collection-item" ] [ text <| "#" ++ name ]

renderChannels : List String -> Html
renderChannels channels =
  let
    channelItems = List.map renderChannel channels
  in
    ul [ class "collection" ] channelItems

view : Signal.Address Action -> Model -> Html
view address model =
  div [ class "card-panel" ] [
    input [ ] [],
    renderChannels model.channels
  ]

We created two new methods renderChannel, which renders an individual li representing a channel with the hash symbol (#), and renderChannels which uses a List.map to return a list of li elements. We then pass that list as the second argument of ul. Lastly, we call renderChannels from the view function, passing in the model.channels.

Note: If you are wondering about the <| operator: it is a reverse pipe and it means the result of everything on the right of that operator (until the closure) will be piped into the function to the left of the operator. It's just a way to avoid parens.

Filtering the list

We are displaying the list rendered directly from the model, now it's time to filter the view according to the user input in the search box.

First we need to store the filter the user types in on the model. For that we will add an onInput event on the input box, so we can filter the list as the user is typing.

view : Signal.Address Action -> Model -> Html
view address model =
  div [ class "card-panel" ] [
    input [ onInput address Filter ] [],
    renderChannels model.channels
  ]

The Filter action tag provides us with a free "constructor" that takes in a string, that string will be passed in to onInput from the browser as event.target.value or in elm-html targetValue.

Note: onInput is not yet part of the elm-html package, which is why we imported the Html.Events.Extra package which comes from circuithub/elm-html-extra

Then we will use the List.filter method to only display channels starting with the input text.

filterChannels : List String -> String -> List String
filterChannels channels query =
  List.filter (String.contains query) channels

Then use this function in the view function:

view : Signal.Address Action -> Model -> Html
view address model =
  div [ class "card-panel" ] [
    input [ onInput address Filter ] [],
    renderChannels (filterChannels model.channels model.query)
  ]

To make sure that the filter is case insensitive we will need to refactor the filterChannels and pass both the query and list item to String.toLower.

filterChannels : List String -> String -> List String
filterChannels channels query =
  let
    containsCaseInsensitive str1 str2 =
      String.contains (String.toLower str1) (String.toLower str2)
  in
    List.filter (containsCaseInsensitive query) channels

Adding style

For styling the component we will create a new HTML file and include the Materialize CSS library.

This is what your HTML should look like:

<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.5/css/materialize.min.css">
    <script src="channel_switcher.js"></script>
  </head>
  <body>
    <div id="elm-goes-here" class="container"></div>
    <script>
      Elm.embed(
          Elm.ChannelSwitcher,
          document.getElementById('elm-goes-here')
      );
    </script>
  </body>
 </html>

To compile the Elm file into channel_switcher.js use the --output flag:

elm make ChannelSwitcher.elm --output channel_switcher.js

When you open the HTML file you should see something like this:

Final

And here is our Elm code so far:

module ChannelSwitcher where

-- IMPORTS
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Html.Events.Extra exposing (..)
import String
import StartApp.Simple as StartApp

-- MODEL
type alias Model =
  { channels: List String
  , selectedChannel: Int
  , query: String
  }

initialModel : Model
initialModel =
  { channels = ["Elm", "React.js", "Ember", "Angular 2", "Om", "OffTopic" ]
  , selectedChannel = -1
  , query = ""
  }

-- UPDATE
type Action = NoOp | Filter String | Select Int

update action model =
  case action of
    NoOp ->
      model

    Filter query ->
      { model | query = query }

    Select index ->
      { model | selectedChannel = index }


-- VIEW
filterChannels : List String -> String -> List String
filterChannels channels query =
  let
    containsCaseInsensitive str1 str2 =
      String.contains (String.toLower str1) (String.toLower str2)
  in
    List.filter (containsCaseInsensitive query) channels

renderChannel : String -> Html
renderChannel name =
  li [ class "collection-item" ] [ text <| "#" ++ name ]

renderChannels : List String -> Html
renderChannels channels =
  let
    channelItems = List.map renderChannel channels
  in
    ul [ class "collection" ] channelItems

view : Signal.Address Action -> Model -> Html
view address model =
  div [ class "card-panel" ] [
    input [ onInput address Filter ] [],
    renderChannels (filterChannels model.channels model.query)
  ]

main : Signal Html
main =
  StartApp.start
    { model = initialModel
    , update = update
    , view = view
    }

What's Next?

In the next post I will build a version of this component utilizing Messages, Effects and Ports. This will allow us to add keyboard interaction and JavaScript interop.

  • Adobe logo
  • Barnes and noble logo
  • Aetna logo
  • Vanderbilt university logo
  • Ericsson logo

We're proud to have launched hundreds of products for clients such as LensRentals.com, Engine Yard, Verisign, ParkWhiz, and Regions Bank, to name a few.

Let's talk about your project