Working with Events
the JavaScript library StimulusReflex rocks because it stands on the shoulders of Stimulus
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 JavaScript library has everything it needs to present a favorable alternative to using SPAs. They are 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 they can keep the footprint of the JavaScript library so small without sacrificing functionality is that it is tightly integrated with Stimulus, a lightweight library that provides powerful event handling.
They also draw upon proven libraries such as Lodash when necessary to craft flexible solutions to common problems.
Throttle, Debounce and requestAnimationFrame
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.
debounce is so flexible that the Lodash implementation of throttle is actually implemented using debounce.
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, we strongly recommend that you use the functions found in the Lodash library. Not only are they are flexible, well-tested and optimised, but they actually return new functions that you can assign to replace your existing functions. Once you wrap your head around the power this provides, other approaches will feel like dirty hacks.
If you yarn add lodash-es
you will be able to use a version of the library that supports tree shaking. This means that Webpack will only grab the minimum code required, keeping your production JS bundle size tiny.
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.
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 executes the scroll method and it can do whatever you would like it to do.
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
.
Just before we move on, 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.
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.
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:
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 w
one time:
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.
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, TurboLinks and ActionCable.
Real-world examples
keydown throttle
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:
Last updated