Mathias Polligkeit
  • Dev
  • Impro
  • Sheet Music
  • Contact
Oct 23, 2017 (last updated: May 15, 2022)

Handling Single, Double, Triple, Quadruple Clicks in Elm

Elm has onClick and onDoubleClick event handlers, but what if you need to handle triple, quadruple or any number of clicks?

We’re going to build a simple program that displays multiple boxes containing multiple rows containing multiple items. We want different behaviors for different numbers of clicks.

  • one click: select one item
  • two clicks: select all items in a row
  • three clicks: select all items in a box
  • four clicks: select all items

This tutorial was written for Elm 0.19.

Initial Setup

Create a file called Main.elm and insert the following lines.

module Main exposing (..)

import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Json.Decode exposing (..)


main : Program () Model Msg
main =
    Browser.sandbox
        { init = init
        , update = update
        , view = view
        }

Our model will only contain our current selection. We’ll use a union type to define it.

type Selection
    = All
    | Box Int
    | Row Int Int
    | Item Int Int Int
    | Clear


type alias Model =
    Selection


init : Model
init =
    Clear

The first Int of Box, Row and Item defines the box, the second Int of Row and Item defines the row, and the last Int of Item defines the item. We should probably use type aliases to make this clearer:

We’re going to define a Select message that takes a Selection and add an update function to update our model.

type Msg
    = Select Selection


update : Msg -> Model -> Model
update msg model =
    case msg of
        Select selection ->
            selection

To complete our sandbox program, we also need a view function.

view : Model -> Html Msg
view model =
    div
        [ classList
            [ ( "main", True )
            , ( "selected", model == All )
            ]
        ]
        (List.map
            (box model)
            (List.range 1 3)
        )

box : Selection -> Int -> Html Msg
box selection boxId =
    div
        [ classList
            [ ( "box", True )
            , ( "selected", selection == Box boxId )
            ]
        ]
        (List.map
            (row selection boxId)
            (List.range 1 3)
        )


row : Selection -> Int -> Int -> Html Msg
row selection boxId rowId =
    div
        [ classList
            [ ( "row", True )
            , ( "selected", selection == Row boxId rowId )
            ]
        ]
        (List.map
            (item selection boxId rowId)
            (List.range 1 3)
        )


item : Selection -> Int -> Int -> Int -> Html Msg
item selection boxId rowId itemId =
    div
        [ classList
            [ ( "item", True )
            , ( "selected", selection == Item boxId rowId itemId )
            ]
        ]
        []

This will render two boxes with three rows each that in turn contain three items each. Boxes, rows and items are identified by integers. We are using List.map and List.range to avoid typing it all out.

To add the classes, we’re using Html.Attributes.classList, which takes a list of tuples with the first element being the class name and the second element being a bool value.

We always want to give boxes, rows and items the class box, row and item respectively, so we just pass True as the second element. But we only want to add the class selected if the box, row or item is actually selected, so we check if the current selection matches the current element.

This completes the static version of our program. To compile, run:

elm make Main.elm --output=main.js

HTML

Create the file index.html with the following content.

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Handling Triple, Quadruple (Or More) Clicks In Elm</title>
    <style>
      html,
      body,
      #elm,
      .main {
        height: 100%;
        margin: 0;
      }
      .main {
        -webkit-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;

        display: flex;
        justify-content: space-between;
        align-items: stretch;
      }

      .box {
        flex: 0 1 30%;
        display: flex;
        flex-direction: column;
        justify-content: space-around;
        align-items: stretch;
      }

      .row {
        flex: 0 1 30%;
        display: flex;
        flex-direction: row;
        justify-content: space-around;
        align-items: stretch;
      }

      .row div {
        flex: 0 1 30%;
        background: #00caf5;
      }

      .selected .item,
      .item.selected {
        background: #002827;
      }
    </style>
  </head>

  <body>
    <div id="elm"></div>
    <script src="main.js" type="text/javascript"></script>
    <script type="text/javascript">
      var app = Elm.Main.init({
        node: document.getElementById("elm")
      });
    </script>
  </body>
</html>

This will embed our Elm app and add some styling. If you open the file in a browser, you should see three columns with nine bland, blue boxes each.

Click Handling

Since Elm doesn’t have a function to handle multiple clicks, we’ll need to write our own function. We’re going to use the Html.Events.on function for this. Let’s add it to our item attributes.

item : Selection -> Int -> Int -> Int -> Html Msg
item selection boxId rowId itemId =
    div
        [ on "click" (handleClick boxId rowId itemId)
        , classList
            [ ( "item", True )
            , ( "selected", selection == Item boxId rowId itemId )
            ]
        ]
        []

on takes the event name as the first argument (in our case click) and a Decoder msg as the second argument. The click event has a whole bunch of properties, and we need the JSON decoder to access them. In our application, we’re interested in the vaguely named “detail” property, which contains the number of clicks that happened within a short time frame.

Our handleClick function will also need the current box, row and item ID to make a new selection.

handleClick : Int -> Int -> Int -> Decoder Msg
handleClick boxId rowId itemId =
    maybe (field "detail" int)
        |> andThen (clickMsg boxId rowId itemId)

Just in case the browser doesn’t support the detail property, we’re decoding the detail field as a Maybe Int. This Maybe Int will then be passed to clickMsg, which will give us a Msg depending on the number of clicks.

clickMsg : Int -> Int -> Int -> Maybe Int -> Msg
clickMsg boxId rowId itemId s =
    case s of
        Just 1 ->
            Select (Item boxId rowId itemId)

        Just 2 ->
            Select (Row boxId rowId)

        Just 3 ->
            Select (Box boxId)

        Just _ ->
            Select All

        Nothing ->
            Select (Item boxId rowId itemId)

In case the browser doesn’t pass a proper detail property, we want to select a single item only, so we still have some basic functionality. Because case needs branches for all possibilities, we need to define a Just _ branch to handle more than three clicks in a row. We want to select all items in that case, so that all items are being selected even if the user accidentally clicks too often.

Let’s compile this again:

elm make Main.elm --output=main.js

Our app should now behave as per spec, but we can clean this up a little bit.

Cleaning Up

We’re going to replace Json.Decode.andThen with Json.Decode.map first. This will save us the call to Json.Decode.succeed and a pair of brackets.

handleClick : Int -> Int -> Int -> Decoder Msg
handleClick boxId rowId itemId =
    maybe (field "detail" int)
        |> Json.Decode.map (clickMsg boxId rowId itemId)


clickMsg : Int -> Int -> Int -> Maybe Int -> Msg
clickMsg box row item s =
    case s of
        Just 1 ->
            Select (Item box row item)

        Just 2 ->
            Select (Row box row)

        Just 3 ->
           Select (Box box)

        Just _ ->
            Select All

        Nothing ->
            Select (Item box row item)

That’s better, but it’s kind of useless to pass box, row and item to our handleClick function just to pass it along without doing anything with it. This also makes reusing handleClick harder. Let’s rename the function to onMultipleClick and move the call to Html.Events.on from the view to this function.

item : Selection -> Int -> Int -> Int -> Html Msg
item selection boxId rowId itemId =
    div
        [ onMultiClick (clickMsg boxId rowId itemId)
        , classList
            [ ( "item", True )
            , ( "selected", selection == Item boxId rowId itemId )
            ]
        ]
        []



onMultiClick : (Maybe Int -> msg) -> Attribute msg
onMultiClick intToMsg =
    maybe (field "detail" int)
        |> Json.Decode.map intToMsg
        |> on "click"

Now we only need to pass a function with the signature (Maybe Int -> msg) to our new onMultiClick function. That way, it is not bound to this specific use case and can be reused anywhere. Also note the use of msg instead of Msg in the type signature. In doing so, we can factor the function out and use it in any package.

The result

You can find a working demo at woylie.de/elm-triple-click-example and the complete code example on Github.

  • elm

See Also

  • Life in Weeks
  • Running Pace Calculator
  • Live Coding: Swarm Behaviour in Elm
  • privacy policy