This post describes the common functionality of event handling in a project I’m working on for fun.

I’ve spent my career trying to stay comfortably between cryptic and verbose. I recently started a project for fun using a javascript library I first wrote over 15 years ago. It has looked like jQuery. It has looked like vanilla JS.

I’ve had versions with lots of magic even though I usually stay away from magic (too many issues with maintenance and extensibility and not being able to figure out why that app from 5 years ago works).

I had a few objectives with this version

The first is always my requirement for a new library/module/reusable-thing:

“simple things should be simple. complicated things should be possible.”

I also had a couple of typical requirements that I intentionally ignored for this project

So far I have not seen any issues on many browsers, a few laptops, and even tablets and phones.

I decided to go verbose. Event handlers are all based on EventHandler. A simple mouse listener may look like this

    BuildMouseListener()

        .listen(‘#watch’)

        .onMouseMove(simpleMouseMoveFunction)

        .onMouseDown(simpleMouseDownFunction)

        .onMouseUp(simpleMouseUpFunction)

        .build();

Or, in an extreme example

    BuildMouseListener()

        .listen(‘#example’)

        .selector(‘.important-element-class’)

        .setDebounceMSecs(50)

        .withCtrl()

        .capture()

        .continuation(Continuation.PreventDefault)

        .filterAllow(event => { return event.type == ‘wheel’ || event.clientX < 200; })

        .filterExclude(event => { return event.type != ‘wheel’ && event.clientY > 800; })

        .setData(getDataFromEvent)

        .onMouseMove(simpleMouseMove)

        .onMouseDown(mouseDown)

        .onMouseUp(mouseUp)

        .onMouseOver(mouseOver)

        .onMouseOut(mouseOut)

        .onMouseWheel(mouseWheel)

        .build();

I’ve tried including most of these options as varargs in a constructor before. And I’ve made some magic to usually do the right thing no matter what is passed. Completely unmaintainable over many projects and years even with an “options” object for the rare ones.

I’ve also implemented generic functions for things like debouncing, filtering, and collecting data. Those also have maintenance problems. Plus, I forget what I built last year and build it again.

In this implementation, the first 9 properties above are available to ALL event listeners, even custom events and events. With modern IDEs, it doesn’t take much typing, and intellisense makes it easy to see what options are available.

The Basics

Every group of event types has 3 things

  1. A listener class derived from EventListener (MouseListener)
  2. A builder class derived from HandlerBuilder (MouseHandlerBuilder)
  3. A builder function to create a listener. (BuildMouseListener)

Handlers for a group of events are very easy to write. There are 2 required methods

  getEventTypes() { return [“mousemove”,”mousedown”,…];}

  callHandlers(event) {…}

I will write more about these in another post, but I have helpers to make callHandlers() easy.

HandlerBuilders are also easy. The base class takes care of most things. HandlerBuilders only need to deal with event-type-specific things like registering application callbacks.

I group events into handlers because they usually have common needs. KeyHandler and MouseHandler use different parts of the javascript Event object, but most key events and most mouse events do the same things.

I currently have handlers for these categories of events

listen() & selector()

The listen method of an EventBuilder sets which DOM element is used for addEventListener(). It defaults to the body and captures any event that bubbles up to this level.

selector() is used to set the element an event originates from. Any selector that is valid in DOM querySelector() calls is valid.

I can listen to all checkboxes in my photo-container element with

HandlerBuilder().listen(‘#photo-container’).selector(‘input[type=”checkbox”]);

One of the places I have reduced verbosity is allowing selector to be passed to listen()

I can listen to all checkboxes in my photo element with

HandlerBuilder().listen(‘#photo-container’, ‘input[type=”checkbox”]);

When they are both included, they should be reviewed as a unit to understand correctness.

filterAllow() & filterExclude()

These 2 methods can be used to handle or ignore events based on anything the application wanted (event values, application state, time of day, …) Only one is necessary, but this way the name makes it easy to know what should be returned: true to allow, or true to exclude.

withCtrl(), withAlt(), withShift()

These are filters that require one or more keys to be pressed for the event to be processed. One place I use it is the mouse wheel

BuildWheelHandler().withAlt().onChange(zoom);

BuildWheelHandler().withCtrl().onChange(pan);

continuation()

Somehow I mess up Event.stopPropagation(), Event.stopImmediatePropagation() and Event.preventDefault() more than I should. To help get that right, I have a Continuation class with a boolean flag for each of those. I also have easy access to objects that do the things I want

Continuation.StopAll
Continuation.StopPropagation
Continuation.StopPropagationImmediate
Continuation.PreventDefault
Continuation.Continue

Any of these can be set as the default for a Listener and the correct Event stop/prevent functions will be called if the event is handled (e.g. not filtered or debounced). In addition, any application handler function can return a Continuation and it is used instead of the default. Any non-Continuation return does nothing.

There is currently no good way to manage continuation if an event is no processed because of a filter or debouncing. A filterAllow could be used to explicitly call the Event functions but I haven’t had the need.

Debouncing

This is mainly useful for user input when you don’t want to do an action until input stops. For example, don’t change auto-complete options until the user pauses typing. This example won’t call doAutoComplete until the user has not pressed a key for 300 milliseconds. At that point the most recent event is processed (all other are thrown out)

BuildKeyListener().setDebounceMSecs(300).onKey(doAutoComplete)

I also use it for some custom events. I can listen to all changes to a list, but not do anything until changes stop.

setData()

The first argument to handler functions is usually a type-specific value (e.g. mouse position, key, input value). In some cases I have many handlers that all need the same data.

For example, in my current application, every photo container has an ID

<div class=’photo” data-file-id=”123″>

<img><input><input><span>…

</div>

I have many handlers listening to different elements in the <div> Instead of each handler finding “.photo” element and the “file-id” data value and getting the MediaFile for that ID, there is one function attached to all handlers. It would be as easy to call it when needed but you can also use setData()

BuildCheckboxListener()
.setData(getFile)
.selector(‘input.select’)
.onCheck(selectFile)
.unUncheck(unselectFile)
BuildInputListener()
.setData(getFile)
.selector(‘input.name’)
.onChange(renameFile)

selectFile(mediaFile) {…}
unselectFile(mediaFile) {…}
rename(mediaFile, value) {…}

I’m not convinced it’s useful but I have liked it so far.

setCapture(), setPassive(), once()

I rarely use these, and when I want them I need to look it up. Not a big deal, but now it’s trivial to add options to addEventListener. Also, I won’t look at an addEventListener(…,true) and have to think about what it means.

Next

This post explained the common functionality available to all of my new event handlers. Future posts will describe specific handlers (input, mouse, key, etc). Let me know on twitter if you have any questions of there’s anything you’d like to see me cover.

My primary purpose for these posts is to work on my writing skills. I’m always happy to get feedback (good, bad, constructive or not) about my code, and hope for some on my writing. Feedback/criticism is the best way to improve – even feedback I consider and reject.

The application I wrote this for is on github. And I’m working on simplified examples.