Tutorial - Input - Handling Events

Source Code

The source code for this example is included in the tree state-button in the tutorial repository.

Input

In order to receive input from the user, components must be able to connect to DOM events. Backdraft provides a thin layer of syntax sugar and bookkeeping help, but does not redefine or interfere with the native DOM event system. We'll look at that machinery and a few other features in this section.

A State Button

To exercise several Backdraft features, we're going to build a button that progresses through a cycle of states each time it is clicked. Here are the requirements for the component.

  • It looks like a button and has an label that is displayed in the middle of the button. A getter/setter is included to mutate the label.
  • The button maintains an ordered sequence of states; this is accessible through a getter/setter at states.
  • At any point in time, the button has the "value" of one of these states; this is accessible through a getter/setter at value.
  • It signals its clients when its value changes.
  • The button can optionally change appearance depending on its value.
  • Clicking the button causes the value to change to the next value in the states sequence. The last value in states circles back to the first value.
  • It can receive the keyboard focus, and when it has the focus, pressing the space bar behaves like a click.
  • It signals its clients when a value change is demanded by a click or space bar event.
  • It can be disabled so that it neither receives the focus nor reacts to click events.

The User Interface Implementation

We'll implement the button as a textNode nested in a div nested in a div. This will allow for easy formatting (e.g., centering) of the text. Further, we'll use the default states of {"on", "off"}. The component will include "bd-state-button" and the current value in the classList on the top-level div. Lastly, when the component is rendered, we'll connect a click event listener to the top-level div node and advance the value upon receiving a click event. Here is the first attempt:

import {Component, e, connect, stopEvent} from "./backdraft.js"

export default class StateButton extends Component {
	constructor(kwargs){
		super(kwargs);
		this._label = "label" in kwargs ? kwargs.label : "Button";
		this._states = kwargs.states || ["on", "off"];
		this.value = "value" in kwargs ? kwargs.value : this._states[0];
	}

    get label(){
		return this._label;
	}

    set label(value){
        if(this._applyWatchers("label", "_label", value) && this.rendered){
            this._labelNode = value;
        }
    }

	get value(){
		return this._value;
	}

	set value(newValue){
		if(newValue !== this._value){
			let index = this._states.indexOf(newValue);
			if(index !== -1){
				let oldValue = this._value;
				this._value = newValue;
				this.className = newValue + "";
			}else{
				console.error("illegal value");
			}
		}// else, no change, ignore
	}

    _elements(){
        return e("div",
            {
                className: "bd-state-button",
                [e.advise]: {"click": this._onClick.bind(this)}
            },
            e("div", {[e.attach]: "_labelNode"}, this._label)
        )
    }

	_onClick(e){
		stopEvent(e);
		let states = this._states;
		this.value =
			states[(states.indexOf(this._value) + 1) % states.length];
	}
}

StateButton.className = "bd-state-button";

This code is really quite trivial...and that is the point! The only machinery that we haven't seen so far is located in the protected method _elements(); let's take a closer look.

First, notice that props for the top-level element contains the property [e.advise]. The value of e.advise is a symbol defined by Backdraft, and it's presence in props indicates special processing should be executed after the element is rendered. Out of the box, there are six special processing instructions:

  1. [e.attach] - stores the DOM node or Component instance created during rendering at an instance property.
  2. [e.advise] - connects one or more event listeners to events.
  3. [e.watch] - connects one or more property watchers; applies only to elements that create Component instances.
  4. [e.applyMethod] - applies an instance method after instantiation; applies only to elements that create Component instances.
  5. [e.tabIndexNode] - causes the tabIndex property to be applied to the given node in the Component's rendered element tree; applies only to elements that create DOM nodes.
  6. [e.titleNode] - causes the title property to be applied to the given node in the Component's rendered element tree; applies only to elements that create DOM nodes.

Each of e.attach, e.advise, e.watch, e.apply, e.tabIndexNode, and e.titleNode hold a symbol created by Backdraft. Therefore, it is impossible for any of these properties to clash with a property or attribute you might want to initialize when a DOM node is created or some constructor keyword argument when a component instance is created.

Backdraft includes machinery that allows programmers to extend this list with their own special processing instructions; that's an advanced topic that we'll cover later.

[e.advise] takes a JavaScript object that gives a map from event names to listener functions. The special processing connects the listener to the event. In this example, a div DOM node is created, so the list of possible names are the events defined by a div node as given by the DOM API (not the Backdraft API). Our code is interested in the click event. So, the line...

[e.advise]: {"click": this._onClick.bind(this)}

Is roughly equivalent to this...

let node = render(e("div"));
node.addEventListener("click", node._onClick.bind(node))

The Backdraft takes care of applying the underlying DOM API functions to connect the event listener and then remove the event listener when the instance is unrendered. There is never the need to manually track connection handles!

[e.attach]:name places a reference to the rendered Element at the instance property name. For StateButton, we need a reference to the inner div node so the textNode child of that node can be set to the value of the label property.

Events

It is usually a bad idea to tightly couple side effects a component's features by executing those side effects directly in the code that implements those features. Consider a button component. Buttons exist to be clicked and when they are clicked, something should happen. If that something is mentioned directly in the click handler, then the button component can only be used for that one purpose. There are two ways to decouple side effects from core component features: specialization and configuration.

Specialization is implemented by creating a subclass of the more general component. Here's how to specialize a button component so that it executes a particular process when an instance of the button component is clicked:

class SpecializedStateButton extends StateButton {
	_onClick(e){
		super._onClick(e);
		doSomethingSpecial();
	}
}

While this works correctly, it is unsatisfying: it's a lot of work to create a new type for such a common and trivial specialization. Instead, we'd like some machinery that allowed clients of button components to be signaled on a click event. Clients can then decide whether or not they want to do something more upon a click, and if so, what to do, all the while the underlying Button component knows nothing of the client's decisions. This results in a loosely-coupled system; such systems decrease complexity (concerns are separated), and, therefore, are less expensive to construct.

Adding a single line to StateButton's _onClick handler causes StateButton instances to signal clients when they are pushed:

_onClick(e){
    stopEvent(e);
    let states = this._states;
    this.value =
        states[(states.indexOf(this._value) + 1) % states.length];

    //...this allows clients to connect to an "onClick" event
    this._applyHandlers({name: "onClick", domEventObject: e});
}

Component includes Backdraft's EventHub mixin, which provides an interface to signal events and allow clients to connect to those signals. The instance method _applyHandlers(eventObject) causes the instance to apply all listeners that have registered to the event name given by eventObject.name. Here's how a client would listen for a click event:

let someButton = render(StateButton, document.getElementById("root"));
someButton.advise("onClick", function(e){
	console.log(e);
});

advise(name, listener) applies listener to the eventObject provided by _applyHandlers when eventObject.name===name. Notice that the only requirement of event objects sent by _applyHandlers and received by the listeners is that they include a name property which gives the event name that is used to register listeners. The EventHub functionality included in Component is completely independent of the DOM event system. That said, clients can register listeners to events through [e.advise] in exactly the same manner for either system:

// an Element that will result in a DOM node
e("div", {
	[e.advise]: {
		// therefore, events are DOM events
		"focus": someHandler,
		"blur": anotherHandler,
		"click": andAnotherHandler
	}
});

// an Element that will result in a Component instance
e(SomeComponent, {
	[e.advise]: {
		// therefore, events are fired by the method _applyHandlers
		// click is _not_ a DOM click event
		"funkyEvent": yetAnotherHandler,
		"click": andYetAnotherHandler
	}
});

Watcher Hubs

Watcher hubs are specialized event hubs that signal listeners when an instance property changes value. Component includes Backdraft's WatchHub mixin, which provides an interface to (a) signal watchers when a property value is mutated and (b) allow clients to connect to those signals. In order to make a property watchable, then one of the instance methods _applyWatchers(name, privateName, newValue) or _applyWatchersRaw(name, oldValue, newValue) must be applied when that property is mutated. It is up to the implementation of a particular component to decide which properties are watchable by applying one of these functions at the appropriate time.

The first option, _applyWatchers(name, privateName, newValue) compares the current value of this[privateName] to newValue using strict equivalence; if the values are different, then this[privateName] is set to newValue, all watchers watching name are signaled, and the function returns true; false is returned otherwise. Watchers can be connected through the method watch(name, watcher); name is the name of the property to watch, watcher is a function with the signature (newValue, oldValue). For Example:

class MyComponent extends Component {
	constructor(props){
		super(props);
		this._foo = null;
	}

	get foo(){
		return this._foo;
	}

	set value(newValue){
		this._applyWatchers("foo", "_foo", newValue)
	}
}


let x = new MyComponent();
x.watch("foo", (newValue, oldValue)=>{
	console.log("new:", newValue, ", old:", oldValue);
});

console.log(x.foo); // --> null
x.foo = 3.14;       // --> new: 3.14, old: null
console.log(x.foo); // --> 3.14

The second option, _applyWatchersRaw(name, oldValue, newValue) unconditionally signals watchers. This is useful when other code has already determined whether a potential mutation is really a change (perhaps using a comparison other than ===) and/or there is more to do than just update the protected property. A good example of this is the value setter in the StateButton component we developed above. Our code currently executes two tests before value is updated: a test for an actual change, and then a test for a legal state. In this case, we clearly don't want to unconditionally mutate value since we'd lose the legal state test. Here is the setter updated to allow clients to connect watchers at the property value.

set value(newValue){
    if(newValue !== this._value){
        let index = this._states.indexOf(newValue);
        if(index !== -1){
            let oldValue = this._value;
            this._value = newValue;
            this.className = newValue + "";

            //...this allows clients to watch the "value" property
            this._applyWatchersRaw("value", oldValue, this._value)
        }else{
            console.error("illegal value");
        }
    }// else, no change, ignore
}

Watchers can be set up by Component instances to watch their own properties. This allows expressing patterns like we see in the current implementation of the label getter differently--perhaps with less coupling. Recall, how the label getter is currently expressed:

set label(value){
    if(this._applyWatchers("label", "_label", value) && this.rendered){
        this._labelNode = value;
    }
}

We can move the internal side-effect of updating the contents of the inner div node to a watcher; we'll also have to connect that watcher any time the component is rendered. Here's what that change looks like:

set label(newValue){
	this._applyWatchers("label", "_label", newValue)
}

postRender(){
	return this.watch({
		label: (newValue) => (this._labelNode.innerHTML = newValue)
	});
}

We rather like this pattern for properties with simple semantics like label.

postRender(), if implemented at all, is called immediately after a component instance is rendered. postRender may return disconnect handles, and any handles so returned will be automatically disconnected when the instance is unrendered. We'll say a little more about this next.

Event and Watch Connection Handles

Any time an event listener is connected to an event or a watcher is connected to a property, Backdraft returns a connection handle. A connection handle is an object that contains a single function, destroy() that will disconnect the listener or watcher. The destroy() function is "intelligent" in that it can be called multiple times without causing harm--after the first application, destroy() results in a no-op.

Of course events handlers and watchers connected during rendering with [e.advise] and [e.watch] receive these same handles. Component ensures that when an instance is unrendered, any handle created during rendering is destroyed. In fact, Component includes the method ownWhileRendered(...handles) which will ensure that all handles provided as arguments are automatically destroyed when the component is unrendered. Similarly, Component includes the method own(...handles) which will ensure that all handles provided will be automatically destroyed when the component instance is destroyed.

Focus

All subclasses derived from Component include the property hasFocus which holds a boolean value indicating whether or not the component instance has the focus; the focus property is watchable. This machinery is quite convenient and powerful in many situations. For StateButtons, we can use it to set up keyboard event handlers when a button receives the focus and watch for a space bar, and then remove the handlers when the button loses focus. Here's what that looks like:

postRender(){
    return this.watch({
        label: (newValue) => (this._labelNode.innerHTML = newValue),

        // watch when the instance gets/loses the focus...
        hasFocus: this._focusWatcher.bind(this)
    });
}

_focusWatcher(focus){
	if(focus){
		// getting the focus
		this._keyPressHandle = connect(this._dom.root, "keypress", (e) =>{
			if(e.keyCode===32){
				this._onClick(e);
			}
		})
	}else{
		// losing the focus
		this._keyPressHandle && this._keyPressHandle.destroy();
		delete this._keyPressHandle;
	}
}

This example may be a bit contrived as there would be no harm in setting up keyboard handlers whenever a button is rendered.

Disabling a Component

Some components, like StateButton, include capabilities to receive stimulus from the user--typically mouse and keyboard events. Often is is desirable include a feature to disable the component. By disable, we main to make the component completely nonresponsive to user stimuli.

Component includes the public getter/setter enabled which controls the private property [Component.ppEnabled]. The code is trivial:

get enabled(){
	return this[ppEnabled];
}

set enabled(value){
	this._applyWatchers("enabled", ppEnabled, !!value);
	this[value ? "removeClassName" : "addClassName"]("bd-disabled");
}

bd-disabled can be used to visually style a disabled component with CSS and the behavior of a component can be varied depending on the value of this[enabled]. Here's how that would look in StateButton:

_onClick(e){
	stopEvent(e);

    // implement enable/disable functionality...
	if(!this.enabled) return;

	let states = this._states;
	this.value =
		states[(states.indexOf(this._value) + 1) % states.length];
	this._applyHandlers({name: "onClick", domEventObject: e});
}

Pretty nice for one line of code!