handling-user-input_00 By Richard Feldman

This article has been excerpted from Elm in Action.

 

 

How do we keep data flow manageable as our code scales?

JavaScript offers a staggering selection of data flow architectures to choose from, but Elm has only one. It’s called the Elm Architecture, and the Elm Runtime is optimized for applications that follow it. We’ll learn about the Elm Architecture as we add interactivity to Photo Groove.

Figure 1 shows a preview of the architecture we’ll be building toward in this article. Don’t worry if this doesn’t make sense yet! We’ll get there, one step at a time.


handling-user-input_01

Figure 1 The Elm Runtime uses the Elm Architecture to manage data flow


Let’s begin where data flow naturally begins: with application state as a whole.


Representing Application State with a Model

Back in the Wild West days of the Web, it was common to store application state primarily in the DOM itself. Is that menu expanded or collapsed? Check whether one of its DOM nodes has class="expanded" or class="collapsed". Need to know what value a user has selected in a dropdown? Query it out of the DOM at the last possible instant.

This approach turned out to scale poorly, particularly as applications grew more complex and unit testing became increasingly important. Today it’s common practice to store application state completely outside the DOM, and to propagate changes from that independent state over to the DOM as necessary. This is what we do in the Elm Architecture.

Declaring a Model

We’re going to store our application state separately from the DOM, and refer to that state as our model.

Definition A model stores the current state of an application. Any value necessary to render a user interface should be stored in its model.


Listing 1 Adding a Model

	
initialModel = 
    [ { url = "1.jpeg" }   ❶  
    , { url = "2.jpeg" }   ❶  
    , { url = "3.jpeg" }   ❶  
    ]   
  
main = 
    view initialModel      ❷ 

❶   We’ll add more fields beyond url later

❷   Pass our new initialModel record to view


Excellent! Now we have an initial model to work with. It contains a list of photos, each of which is represented by a record containing a url string.

Writing a view function

Next we’ll render a thumbnail for each photo in our list. A typical Elm application does this through a view function, which describes how the DOM should look based on its arguments.

At the top of a typical Elm application is a single view function, which accepts our current model as an argument and then returns some Html. The Elm Runtime takes the Html returned by this view function and renders it.

This will be easier to do if we first write a separate viewThumbnail function, which renders a single thumbnail as Html. We can set the stage for that design by with the following view implementation:


Listing 2 Splitting out viewThumbnail

urlPrefix =                         ❶  
    "http://elm-in-action.com/"     ❶  
  
  
view model = 
    div [ class "content" ] 
        [ h1 [] [ text "Photo Groove" ] 
        , div [ id "thumbnails" ] [] 
        ] 
    
viewThumbnail thumbnail = 
    img [ src (urlPrefix ++ thumbnail.url) ] []  ❷ 

❶   We’ll prepend this to strings like “1.jpeg”

❷   Prepend urlPrefix to get a complete URL like “http://elm-in-action.com/1.jpeg”


Figure 2 illustrates how our current model and view connect to the Elm Runtime.


handling-user-input_02

Figure 2 Model and view connecting with the Elm Runtime


Next, we’ll iterate over our list of photo records and call viewThumbnail on each one, in order to translate it from a dusty old record to a vibrant and inspiring img.

Fortunately, the List.map function does this.


List.map

List.map is another higher-order function similar to the List.filter function. You pass List.map a translation function and a list, and it runs that translation function on each value in the list. Once that’s done, List.map returns a new list containing the translated values.

Take a look at Figure 3 to see List.map do its thing for viewThumbnail.


handling-user-input_03

Figure 3 Using List.map to transform photo records into img nodes


Because div is a plain Elm function that accepts two lists as arguments—a list of attributes, followed by a list of child nodes—we can swap out our entire hardcoded list of child img nodes with a single call to List.map! Let’s go ahead and do that now.

view model = 
    div [ class "content" ] 
        [ h1 [] [ text "Photo Groove" ] 
        , div [ id "thumbnails" ] (List.map viewThumbnail model) 
        ] 
    
viewThumbnail thumbnail = 
    img [ src (urlPrefix ++ thumbnail.url) ] []

If you run elm-make PhotoGroove.elm --output elm.js again to recompile this code, you should see the same result as before. The difference is that now we have a more flexible internal representation, allowing us to add interactivity in a way that was impossible before we connected model and view.

Expanding the Model

Now let’s add a feature: when the user clicks on a thumbnail, it’ll become selected—indicated by a blue border surrounding it—and we’ll display a larger version of it beside the thumbnails.

To do this, we first need to store which thumbnail is selected. That means we’ll want to convert our model from a list to a record, to store both the list of photos and the current selectedUrl value at the same time.


Listing 3 Converting the model to a record

initialModel = 
    { photos = 
        [ { url = "1.jpeg" } 
        , { url = "2.jpeg" } 
        , { url = "3.jpeg" } 
        ] 
    , selectedUrl = "1.jpeg"  ❶  
    }

❶   Select the first photo by default


Next let’s update viewThumbnail to display the blue border for the selected thumbnail.

That’s easier said than done. Being a lowly helper function, viewThumbnail has no way to access the model—so it can’t know the current value of selectedUrl. But without knowing which thumbnail is selected, how can it possibly know whether to return a selected or unselected img?

It can’t! We’ll have to pass that information along from view to viewThumbnail.

Let’s rectify this situation by passing selectedUrl into viewThumbnail as an additional argument. Armed with that knowledge, it can situationally return an img with the "selected" class—which our CSS has already styled to display with a blue border—if the url of the given thumbnail matches selectedUrl.

viewThumbnail selectedUrl thumbnail = 
    if selectedUrl == thumbnail.url then 
        img 
            [ src (urlPrefix ++ thumbnail.url) 
            , class "selected" 
            ] 
            [] 
    else         img 
            [ src (urlPrefix ++ thumbnail.url) ] 
            []

Comparing our then and else cases, we see quite a bit of code duplication. The only thing different about them is whether class "selected" is present. Can we trim down this code?

Absolutely! We can use the Html.classList function. It builds a class attribute using a list of tuples, with each tuple containing first the desired class name, and second a boolean for whether to include the class included in the final class string.

Let’s refactor our above code to the following, which accomplishes the same thing:

viewThumbnail selectedUrl thumbnail = 
    img 
        [ src (urlPrefix ++ thumbnail.url) 
        , classList [ ( "selected", 
selectedUrl == thumbnail.url ) ] 
        ] 
        []

Now all that remains is to pass in selectedUrl, which we can do with an anonymous function. While we’re at it, let’s add another img to display a larger version of the selected photo.


Listing 4 Rendering Selected Thumbnail via anonymous function

	
view model = 
    div [ class "content" ] 
        [ h1 [] [ text "Photo Groove" ] 
        , div [ id "thumbnails" ] 
            (List.map (\photo -> viewThumbnail model.selectedUrl photo) 
                model.photos 
            ) 
        , img    ❶  
            [ class "large" 
            , src (urlPrefix ++ "large/" ++ model.selectedUrl) 
            ] 
            [] 
        ]

❶   Display a larger version of the selected photo


If you recompile with the same elm-make command as before, the result should now look like Figure 4.


handling-user-input_04

Figure 4 Rendering the selected thumbnail alongside a larger version.


Looking good!

Replacing Anonymous Functions with Partial Application

Although the way we’ve written this works, it’s not quite idiomatic Elm code. The idiomatic style would be to remove the anonymous function:

Before: List.map (\photo -> viewThumbnail model.selectedUrl photo) model.photos
After:  List.map           (viewThumbnail model.selectedUrl)       model.photos

Whoa! Does the revised version still work? Do these two lines somehow do the same thing?

 

It totally does, and they totally do! This is because calling viewThumbnail without passing all of its arguments is an example of partially applying a function.

 

Definition Partially applying a function means providing one or more of its arguments, and getting back a new function which accepts the remaining arguments and finishes the job.

 

When we called viewThumbnail model.selectedUrl photo, we provided viewThumbnail with both arguments needed to return Html. If we call it without that second photo argument, what we get back isn’t Html, but rather a function—specifically a function that accepts the missing photo argument and then returns some Html.

 

Let’s think about how this would look in JavaScript, where functions aren’t set up to support partial application by default. If we’d written viewThumbnail in JavaScript, and wanted it to support partial application, it would look like this:

	
function viewThumbnail(selectedUrl) { 
    return function(thumbnail) { 
        if (selectedUrl=== thumbnail.url) { 
            // Render a selected thumbnail here 
        } else { 
            // Render a non-selected thumbnail here 
        } 
    }; 
}

Functions that can be partially applied, such as the one in this JavaScript code, are known as curried functions.

Definition A curried function is a function that can be partially applied.

All Elm functions are curried. That’s why when we call (viewThumbnail model.selectedUrl) we end up partially applying viewThumbnail, but not getting an undefined argument or an error.

In contrast, JavaScript functions aren’t curried by default. They are, instead, tupled –which means they expect a complete “tuple” of arguments. (In this case, “tuple” refers to “a fixed-length sequence of elements,” not specifically one of Elm’s Tuples.)

Elm and JavaScript both support either curried or tupled functions. The difference is which they choose as the default:

  • In JavaScript, functions are tupled by default. If you’d like them to support partial application, you can first curry them by hand—like we did in our JavaScript viewThumbnail implementation above.
  • In Elm, functions are curried by default. If you’d like to partially apply them…go right ahead! They’re already set up for it. If you’d like a tupled function, write a curried function that accepts a single Tuple as its argument, then destructure that tuple.

Table 1 shows how to define and use both curried and tupled functions in either language.


Table 1 Curried functions and Tupled functions in Elm and JavaScript

Elm JavaScript
Curried Function
splitA separator str = 
    String.split separator str

 

function splitA(sep) { 
  return function(str) { 
    return str.split(sep); 
  } 
}
Tupled Function
splitB ( separator, str ) = 
    String.split separator str
function splitB(sep, str) { 
  return str.split(sep); 
}
Total Application
splitB ( "-", "867-5309" )
splitB("-", "867-5309")
Total Application
splitA   "-"  "867-5309"
splitA("-")("867-5309")
Partial Application
splitA   "-"
splitA("-")

We can use our newfound powers of partial application to make view more concise! We now know we can replace our anonymous function with a partial application of viewThumbnail.

Before: List.map (\photo -> viewThumbnail model.selectedUrl photo) model.photos 
After:  List.map           (viewThumbnail model.selectedUrl)       model.photos

TIP In Elm, an anonymous function like (\foo -> bar baz foo) can always be rewritten as (bar baz) by itself. Keep an eye out for this pattern; it comes up surprisingly often.

Here’s how our updated view function should look.


Listing 5 Rendering Selected Thumbnail via partial application

	
view model =     div [ class "content" ] 
        [ h1 [] [ text "Photo Groove" ] 
        , div [ id "thumbnails" ] 
            (List.map (viewThumbnail model.selectedUrl) model.photos)  ❶  
        , img 
            [ class "large" 
            , src (urlPrefix ++ "large/" ++ model.selectedUrl) 
            ] 
            [] 
        ]

❶   Partially apply viewThumbnail with model.selectedUrl


Because all Elm functions are curried, it’s common to give a helper function more information by adding an argument to the front of its arguments list.

For example, when viewThumbnail needed access to selectedUrl, we made this change:

	
Before: List.map  viewThumbnail                    model.photos 
After:  List.map (viewThumbnail model.selectedUrl) model.photos

Because we added the new selectedUrl argument to the front, we could add it using partial application instead of an anonymous function. This is a common technique in Elm code!

TIP Because operators are functions, you can partially apply them too! List.map ((*) 2) [ 1, 2, 3 ] evaluates to [ 2, 4, 6 ].

Incidentally, currying is named after acclaimed logician Haskell Brooks Curry. The Haskell programming language is also named after his first name, and whether the Brooks Brothers clothing company is named after his middle name is left as an exercise to the reader.


Handling Events with Messages and Updates

Now that we can properly render which thumbnail is selected, we need to change the appropriate part of the model whenever the user clicks a different thumbnail.

If we were writing JavaScript, we might implement this logic by attaching an event listener to each thumbnail:

thumbnail.addEventListener(“click”, function() { model.selectedUrl = url; });

Elm wires up event handlers a bit differently. Similarly to how we wrote a view function that used Virtual DOM nodes to describe our desired page structure, we’re now going to write an update function that uses messages to describe our desired model.

Definition A message is a value used to pass information from one part of the system to another.

When the user clicks a thumbnail, a message will be sent to an update function as follows:


handling-user-input_05

Figure 5 Handling the event when a user clicks a thumbnail


The format of our message is entirely up to us. We could represent it as a string, or a list, or a number, or anything else we please. Here’s a message implemented as a record:

{ operation = "SELECT_PHOTO", data = "2.jpeg" }

This record is a message which conveys the following information:

“We should update our model to set 2.jpeg as the selectedUrl.”

The update function receives this message and does the following:

  1. Looks at the message it received.
  2. Looks at our current model.
  3. Uses these two values to determine a new model, then returns it.

We can implement our “select photo” logic by adding this update function right above main:

	
update msg model =     if msg.operation == "SELECT_PHOTO" then 
        { model | selectedUrl = msg.data } 
    else 
        model

Notice how, if we receive an unrecognized message, we return the original model unchanged. This is important! Whatever else happens, the update function must always return a new model, even if it happens to be the same as the old model.

Adding onClick to viewThumbnail

We can request that a SELECT_PHOTO message be sent to update whenever the user clicks a thumbnail, by adding an onClick attribute to viewThumbnail:

	
viewThumbnail selectedUrl thumbnail = 
    img 
        [ src (urlPrefix ++ thumbnail.url) 
        , classList [ ( "selected", selectedUrl == thumbnail.url ) ] 
        , onClick { operation = "SELECT_PHOTO", data = thumbnail.url } 
        ] 
        []

The Elm Runtime takes care of managing event listeners behind the scenes, so this one-line addition is the only change we need to make to our view. We’re ready to see this in action!

The Model-View-Update Loop

To wire our Elm application together, we’re going to change main = view model to the following, which incorporates update according to how we’ve set things up.

main = 
    Html.beginnerProgram 
        { model = initialModel 
        , view = view 
        , update = update 
        }

The Html.beginnerProgram function takes a record with three fields:

  • model – A value that can be anything.
  • view – A function that takes a model and returns a Html node.
  • update – A function that takes a message and a model, and returns a new model.

It uses these arguments to return a description of a program, which the Elm Runtime sets in motion when the application starts up. Before we got beginnerProgram involved, main could only render static views. beginnerProgram lets us specify how we want to react to user input!

Figure 6 demonstrates how data flows through our revised application.


handling-user-input_06

Figure 6 Data flowing from the start of the program through the Model-View-Update loop


Notice that view builds fresh Html values after every update. That might sound like a lot of performance overhead, but in practice, it’s almost always a performance benefit!

This is because Elm doesn’t recreate the entire DOM structure of the page every time. Instead, it compares the Html it got this time to the Html it got last time and updates only the parts of the page that are different between the two requested representations.

This approach to “Virtual DOM” rendering, popularized by the JavaScript library React, has several benefits over manually altering individual parts of the DOM:

  • Updates are automatically batched to avoid expensive repaints and layout reflows
  • It becomes far less likely that application state will get out of sync with the page
  • Replaying application state changes effectively replays user interface changes

Because onClick lives in the Html.Events module, we’ll need to import it:

import Html.Events exposing (onClick)

And with that final touch…it’s alive! You’ve now written an interactive Elm application!

The complete PhotoGroove.elm file should look like this:


Listing 6 PhotoGroove.elm with complete Model-View-Update in place

	
module PhotoGroove exposing (..) 
  
import Html exposing (..) 
import Html.Attributes exposing (..) 
import Html.Events exposing (onClick) 
  
  
urlPrefix = 
    "http://elm-in-action.com/" 
  
  
view model = 
    div [ class "content" ] 
        [ h1 [] [ text "Photo Groove" ] 
        , div [ id "thumbnails" ] 
            (List.map (viewThumbnail model.selectedUrl) model.photos) 
        , img 
            [ class "large" 
            , src (urlPrefix ++ "large/" ++ model.selectedUrl) 
            ] 
            [] 
        ] 
    
viewThumbnail selectedUrl thumbnail = 
    img 
        [ src (urlPrefix ++ thumbnail.url) 
        , classList [ ( "selected", selectedUrl == thumbnail.url ) ] 
        , onClick { operation = "SELECT_PHOTO", data = thumbnail.url } 
        ] 
        [] 
  
  
initialModel = 
    { photos = 
        [ { url = "1.jpeg" } 
        , { url = "2.jpeg" } 
        , { url = "3.jpeg" } 
        ] 
    , selectedUrl = "1.jpeg" 
    } 
  
  
update msg model = 
    if msg.operation == "SELECT_PHOTO" then 
        { model | selectedUrl = msg.data } 
    else 
        model 
  
  
main = 
    Html.beginnerProgram 
        { model = initialModel 
        , view = view 
        , update = update 
        }

Let’s compile it once more with elm-make PhotoGroove.elm --output elm.js. If you open index.html, you should be able to click a thumbnail to select it. Huzzah!


handling-user-input_07

Figure 7 Our final Photo Groove application


Figure 8 shows where things ended up.


handling-user-input_08

Figure 8 Our final Elm Architecture setup


Congratulations on a job well done!

For more information on Elm you can download the free first chapter of Elm in Action and see this Slideshare presentation for more details and a discount code.