Skip to content
On this page

Working with Events

It's become progressively easier to work with events in a consistent way across all web browsers. There are still gotchas and awkward idiosyncrasies that would make Larry David squirm, but compared to the bad old days of IE6 - long a nevergreen browser default on Windows - there's usually a correct answer to most problems.

The team behind StimulusReflex works hard to make sure that the library has everything it needs to present a favorable alternative to using SPAs. We're also opinionated about what StimulusReflex shouldn't take on, and those decisions reflect some of the biggest differences from other solutions such as LiveView.

A big part of the reason we can keep the footprint of StimulusReflex so small without sacrificing functionality is that it is tightly integrated with Stimulus, a lightweight library that provides powerful event handling.

We also draw upon proven libraries such as Lodash and debounced when necessary to craft flexible solutions to common problems.

Throttle and Debounce

Some actions with some input devices can trigger enough events in a short period of time that unless you handle them properly, you will massively degrade the performance of your application. Common examples include: moving your mouse, holding down a key on your keyboard, scrolling a webpage and resizing your browser window.

For these use cases, we use a technique known as a throttle. A throttle accepts a stream of events and after allowing the first one to execute immediately, it will discard further events until a specified delay has passed.

If you have a delay of 1000ms and send three events in rapid succession, it will fire the first event, wait one second and then fire the third event.

Other times, you might just want to exercise fine control over exactly when some events are allowed to fire. The most common example is the delayed suggested results you see on sites like Google as you type characters into the search box. Your goal is to hold back events until enough time has passed since the last event has been received.

For these use cases, we can use a technique known as a debounce. The classic mental model is holding open the elevator door for people to board. The elevator can't leave until a few seconds after you let go of the button.

Debounce is flexible. In addition to specifying a delay, additional options can indicate whether the first ("leading") event is fired and whether the last ("trailing") event is fired. Much like an angry, beeping elevator there is also maxWait to provide the amount of time to wait before an interim event is fired, even if new events are still arriving.

INFO

LiveView's debounce implementation accepts blur as a delay value, effectively saying "don't do this until the user leaves this input element".

With Stimulus, we can just define a handler for the blur event and keep the concepts separate.

While you can find many implementations of throttle and debounce on the web, one of the most commonly used implementations can be found in the Lodash library. Lodash has dozens of functions that are flexible, well-tested and optimised. They also return new functions that you can assign to replace your existing functions.

INFO

Lodash implementation of debounce is so flexible that throttle is actually implemented using debounce.

If you yarn add lodash you will be able to import just the functions you need thanks to a process known as tree shaking, where Webpack will only grab the minimum code required, keeping your production JS bundle size tiny. In order for tree shaking to work, you have to use the following import syntax:

javascript
import debounce from 'lodash/debounce'

Tree shaking will not work if you attempt to use { debounce } or forget to specify 'lodash/debounce' - just from 'lodash' will include the entire library.

Let's set up a simple example: we will debounce your page scroll events while keeping your server up-to-date on how far down your user is.

javascript
import ApplicationController from './application_controller.js'
import debounce from 'lodash/debounce'

export default class extends ApplicationController {
  connect () {
    super.connect()
    this.scroll = debounce(this.scroll, 1000)
    window.addEventListener('scroll', this.scroll, { passive: true })
  }

  disconnect () {
    window.removeEventListener('scroll', this.scroll, { passive: true })
  }

  scroll = () => {
    this.stimulate('Event#scroll', window.scrollY)
  }
}
ruby
class EventReflex < ApplicationReflex
  def scroll(value)
    puts value
  end
end
html
<div data-controller="scroll" style="height: 5000px"></div>

We can use the Stimulus Global Events syntax to map window scroll events to the scroll function on a Stimulus controller named event. When the controller is attached to the div at page load, connect is fired, StimulusReflex is instantiated and we use the Lodash debounce function to return a new event handler that will execute when the page is scrolled but then stops scrolling for at least a second. We could set a maxWait option if we were worried about users who just won't stop scrolling, but that's as weird as it sounds and qualifies as premature optimisation.

When the handler is executed, we call stimulate and pass the current scroll offset of the browser window to the server as an integer argument. The server reflex writes the scroll offset to STDOUT or your Rails log file.

We will look at more examples below, but for now just remember that throttle with default parameters has the example same form and syntax as debounce.

debounced

Another excellent option for debouncing events is the debounced library, which creates debounced versions of standard bubbling DOM events. It's been designed to pair exceptionally well with Stimulus and StimulusReflex:

html
<input type="text" data-reflex="debounced:input->Example#work">

You can find all of the details on the debounced GitHub page, but it provides a powerful declared syntax which is the perfect fit for declared Reflexes.

The Four Horsemen aka Key Events

We're going to quickly cover the four primary key-capture events available to the modern JavaScript developer. While they all have their uses, it's quite likely that you're going to stick to one or two of them.

The key thing to remember is that keydown and keyup indicate which key is pressed, while keypress indicates which character was entered. A lowercase "a" will be reported as 65 by keydown and keyup, but as 97 by keypress. An uppercase "A" is reported as 65 by all events.

keydown, keypress and keyup can be declared on any receiver including document. The input event can only be captured for an input, select or textarea HTML element. Choose the right event depending on your needs.

keydown

The lowest-level key capture events are also the only events that can pick up control characters; if you need to know that they are holding down control or even just holding down w to move forward, this is your event.

If you press the Escape key, this is the granularity of data you can obtain:

keyvalue
altKeyfalse
charCode0
code"Escape"
ctrlKeyfalse
key"Escape"
keyCode27
location0
metaKeyfalse
repeatfalse
shiftKeyfalse
which27

While very useful for game development, it doesn't see a lot of use in normal web development because if you access event.target.value it gives you the value of the element (usually a text box) before the key was pressed. Many developers have lost many hairs trying to hunt down bugs on their keydown handlers; don't make the same mistake.

It's common to throttle the rate of events fired when the user holds down a key. In the examples below, we'll look at how to throttle on keydown by testing the repeat attribute to see if one key is being held down.

keypress

Similar to keydown, keypress returns the previous value when you access event.target.value. However, it only fires for keys that product a character value, so for example the Escape key is off-limits, as are Alt, Shift, Ctrl and Meta.

Here's the event data obtained by pressing wone time:

keyvalue
altKeyfalse
charCode119
code"KeyW"
ctrlKeyfalse
key"w"
keyCode119
location0
metaKeyfalse
repeatfalse
shiftKeyfalse
which119

INFO

Note that the keypress event is technically deprecated even if it's still widely used.

keyup

While keyup is the direct counterpart of keydown there are some important differences.

Throttling or debouncing is usually not required as the event doesn't fire until the key has been released.

event.target.value returns the value of the text box as it currently appears, with any new changes reflected.

keyup will not fire if you paste text into an input element. It doesn't care that anything has changed; it's only aware of keys being pressed.

input

Introduced in 1999, the new member of the key event family wasn't available in IE until version 9. As IE 9 also doesn't support Websockets, it's as safe to use as ActionCable and by extension, StimulusReflex.

A close cousin of change and blur, input events can be used to manage the state of input, textarea and select elements. input is fired every time the value of the element changes, including when text is pasted. change only fires when the value is committed, such as by pressing the enter key or selecting a value from a list of options. blur fires when focus is lost, even if nothing changed.

Like keypress, input cannot give you access to non-character keycodes such as Escape. It should not require debounce because the event is not fired until after any change has occurred. You can access event.target.value and see the current value of the element.

However, the real power of input (and it's sister event beforeinput) is that they give you boss powers: the data attribute on the event is a string containing the change made, which could be a single character or a pasted novel. Meanwhile, the inputType attribute tells you what kind of change was responsible for the event being fired. With this information, you have the ability to create a timeline log of all changes to a document and even replay them in either direction later.

Getting into the details of how contenteditable works is far beyond the scope of this document, but you can find more information on what's possible in the W3C Input Events spec.

You might also consider checking out Trix, the editor library created by the team behind Rails, Stimulus, Turbo Drive and ActionCable.

keydown throttle example

First, let's tackle a creative use of throttle. We're going to allow the user to mash their keyboard without spamming the server with Reflex updates. However, we only want to throttle if they are holding down a single key:

javascript
import ApplicationController from './application_controller.js'
import throttle from 'lodash/throttle'

export default class extends ApplicationController {
  connect () {
    super.connect()
    this.throttleKeydown = throttle(this.throttleKeydown, 1000)
  }

  keydown (event) {
    event.repeat
      ? this.throttleKeydown(event)
      : this.stimulate('Event#keydown', event.key)
  }

  throttleKeydown (event) {
    this.stimulate('Event#keydown', event.key)
  }
}
ruby
class EventReflex < ApplicationReflex
  def keydown(key)
    puts key
  end
end
html
<div data-controller="event">
  <input type="text" data-action="keydown->event#keydown">
</div>

requestAnimationFrame()

Just before we wrap up events, there is a third important mechanism modern browsers provide to control time in our applications, and that is requestAnimationFrame().

If you've ever developed games, simulations or visualisations, chances are that you've worked with render loops. For the rest of us, the idea that we can use JavaScript, WebGL and the HTML canvas/SVG elements to create incredible visual results might seem alien. There are many great starter articles including "Anatomy of a video game" on MDN.

requestAnimationFrame() is the mechanism used to control screen draw operations. When paired with keydown and mouse/touch events, complete games with GPU-accelerated graphics are possible. New browser APIs such as HTML5 Bluetooth mean that you could use your Xbox controllers.

INFO

It can be intimidating to start out from nothing with requestAnimationFrame(), which is why excellent libraries such as Greensock are so popular.

While a paid version is available, you can get amazingly far with the free version, specifically if you check out their Timeline primative, which offers an impressive selection of callbacks.

We're living in an era when you can use a high accuracy animation timeline to launch Stimulus controller methods in a way that is scrubbable like a video. That's pretty damn cool.

What might come as a surprise is that clever use of StimulusReflex is theoretically fast enough to keep your game state running live on the server while your client is updating at 60fps. We leave this as an exercise for the reader, but please tell us if you achieve cold fusion.

Released under the MIT License.