Often we need to perform certain actions as a feedback to the input from the keyboard. For example, when we need to navigate within a list up and down or perform an action when the meta key (alt or shift) is pressed.

Binding messages to keys

The most natural and convenient way to specify which action must be performed when a key is pressed is to use a dictionary. We going to use a list of tuples of key codes plus messages. In the function below the first argument is the default (or fallback) action and the second argument is the mapping of keys to messages:

onKeyups : m -> List ( Int, m ) -> Attribute m
onKeyups fallback keysToMsgs =
  let
    msgByKeyCode =
      \kc ->
        Maybe.withDefault fallback <|
          Dict.get kc <|
            Dict.fromList keysToMsgs
  in
    on "keyup" <| Decode.map msgByKeyCode keyCode

Here is what is happening. We have a lambda function msgByKeyCode of one argument kc (for key code). Function on takes a string (the event name) and a Decoder msg. The former will decode an event object (untyped, from js) and return a message to trigger.

We need to get the code of a key that was pressed. keyCode for the rescue! It returns Decoder Int.

Okay, We have a list of pairs of ints and messages. If we take that Decoder Int and map it over using a function of type Int -> Msg we will get a Decoder Msg (this is what Functors for). Our lambda function does exactly this! Awesome, we only need to find a corresponding pair in a list! To do so, it is super convenient to use a Dictionary (simply, a key-value storage) as we can easily get from it. What we do here is that we take this list, turn it into a Dict, first item of a tuple becomes a key, the second one becomes a value. Then we can get from that Dict. However, get returns us Maybe x, otherwise, we would have to deal with nulls or undefineds or handle exceptions or something else. There’s no need for it if we have Maybe type. Okay, so at this stage we get Maybe Msg. How do “extract” it from Maybe? Well, one way is to provide a default or fallback value if Nothing has been found. And we have it! It is famous Noop! In other words, if our user pressed a key, and there’s no such pair with the code, we’re not interested in that key, we do nothing!

It only looks scary at the beginning and the temptation to come back to JS and run the code and check the error in the console is high. But, from my experience, the good way to approach the problem is to follow the type annotations and think as a compiler. If you know what a given function does, it is no brainer anymore.

Okay, enough lyrics, let’s use our function:

ul
  [ onKeyups Noop [ ( 38, MoveSelection Up ), ( 40, MoveSelection Down ) ]
  ]
  -- ...

Handle meta keys

The principle is the same here. However, now we need to send a Message with two booleans. We’re going to send NewItem with the state of Shift and Alt keys:

type Msg
    = NewItem Bool Bool

onKeyupsMeta : m -> List ( Int, Bool -> Bool -> m ) -> Attribute m
  onKeyupsMeta fallback keysToMsgs =
    let
      isShift =
        Decode.field "shiftKey" Decode.bool

      isAlt =
        Decode.field "altKey" Decode.bool

      msgByKeyCode =
        \kc altKey shiftKey ->
          Maybe.withDefault fallback <|
            Maybe.map (\msgFn -> msgFn shiftKey) <|
              Maybe.map (\msgFn -> msgFn altKey) <|
                Dict.get kc <|
                  Dict.fromList keysToMsgs
    in
      on "keyup" <| Decode.map3 msgByKeyCode keyCode isAlt isShift

You can definitely spot familiar parts. The only difference is that we have to use two more decoders for meta keys and map over that Maybe Msg from the dictionary two times. Each map will partially apply NewItem adding info about meta keys. That’s it.

Important part to remember here is that Maybe and Decoder are Functors and NewItem is a function of two arguments: NewItem : Bool -> Bool -> Msg. Here it is in action:

input
  [ type_ "text"
  , onKeyupsMeta Noop [ ( 13, NewItem ) ]
  ]
  []

Again, the key factor here is that we don’t have access to the event object. Instead we provide a decoder that will translate it into a data structure that Elm can work with.

I hope you can find it useful for learning and understanding Elm.