Capturing key-presses in Clojurescript with Closure

Table of Contents

Having good keybindings on your site makes a world of difference for technical users (Twitter and Slack are good examples of this), but writing them yourself has several annoying steps. You have everything you need out of the box with Google Closure, though. Here’s how it goes withotu needing to add any dependencies to your project, since Closure is part of Clojurescript.

Strategy and Solution

The plan is simple:

  1. Set up an event-listener for any keystroke
  2. Pair that listener to check if the keystroke matches one you want
  3. Pair if matches, perform the desired function
  (ns myapp
    (:require [goog.events.KeyCodes :as keycodes]
              [goog.events :as gev])
    ;; import in CLJS is ideomatic for goog classes, by analogy to CLJ Java imports
    (:import [goog.events EventType KeyHandler]))

(defn capture-key
  "Given a `keycode`, execute function `f` "
  [keycode-map]
  (let [key-handler (KeyHandler. js/document)
        press-fn (fn [key-press]
                   (when-let [f (get keycode-map (.. key-press -keyCode))]
                     (f)))]
    (gev/listen key-handler
                (-> KeyHandler .-EventType .-KEY)
                press-fn)))


  (defn reagent-content-fn []
    ;; sets up the event listener
  (capture-key {keycodes/L #(js/alert "Luna Lovegood")
                keycodes/D #(js/alert "Dumbledore")
                keycodes/H #(js/alert "Hermione")
                keycodes/Y #(js/alert "harrY")
                keycodes/R #(js/alert "Ron")})
    ;; ... the actual content that the rest of the fn should produce 
    ;; (like the components that will use the keybinding)
  )

Beware multiple listeners

During the development process remember that the life-cycle of java script listeners is not tied to your Reagent/React items, which means that a couple of re-renders and you are stacking up your listeners with the result that one key press will fire multiple events. So if you’re solving for keys this way it is best to fire your listener in a place that won’t be re-rendered, such as your init, routes, or mount functions.

Why not use a regular JS event listener?

Javascript natively provides document.eventListener; why bother with the Google version? THe answer is browser compatibility, since different browsers implement basic javascript mildly differently. https://developers.google.com/closure/library/docs/events_tutorial#events-in-javascript-and-closure-library

A better solution: re-pressed

Some of our findings resulted in a better awareness of the problem space. Indeed what is above works ok if you are just building a single, uncomplicated key scheme; however, for more involved solutions consider using the re-pressed library. For instance, it allows easier re-binding of keys, and also considers the many-keys-to-one-function scenario as well as the one-key-to-many-functions. Plus, it has a Monty Python quote as its title-line.

Resources

I am always eager for improvements or suggestions!

Tory Anderson avatar
Tory Anderson
Full-time Web App Engineer, Digital Humanist, Researcher, Computer Psychologist