JsAction is a tiny event delegation library that allows decoupling the DOM nodes on which the action occurs from the JavaScript code that handles the action.
The traditional way of adding an event handler is to obtain a reference to the
node and add the event handler to it. JsAction allows us to map between events
and names of handlers for these events via a custom HTML attribute called
jsaction
.
Separately, JavaScript code registers event handlers with given names which need not be exposed globally. When an event occurs the name of the action is mapped to the corresponding handler which is executed.
Finally, JsAction uncouples event handling from actual implementations. Thus one may late load the implementations, while the app is always able to respond to user actions marked up through JsAction. This can help in greatly reducing page load time, in particular for server side rendered apps.
JsAction is built using the Closure Compiler. You can obtain a recent compiler from the site.
JsAction depends on the Closure Library. You can obtain a copy of the library from the GitHub repository.
The compiler is able to handle dependency ordering automatically with the
--only_closure_dependencies
flag. It needs to be provided with the sources and
any entry points.
See the files dispatch_auto.js, eventcontract_auto.js, and eventcontract_example.js for typical entry points.
Here is a typical command line for building JsAction's dispatch_auto.js:
find path/to/closure-library path/to/jsaction -name "*.js" | xargs java -jar compiler.jar \ --output_wrapper="(function(){%output%})();" \ --only_closure_dependencies \ --closure_entry_point=jsaction.dispatcherAuto
If you would like to test out JsAction, you can link precompiled scripts into your page.
<script src="https://www.gstatic.com/jsaction/contract.js"></script>
...
<script src="https://www.gstatic.com/jsaction/dispatcher.js" async></script>
You can play around with JsAction already set up with the following directions at https://jsfiddle.net/q2eacgs7/.
Actions are indicated with the jsaction
attribute. They are separated by ;
,
where each one takes the form:
[eventType:]<namespace>.<actionName>
If an eventType
is not specified, JsAction will assume click
.
<div id="container">
<div id="foo"
jsaction="leftNav.clickAction;dblclick:leftNav.doubleClickAction">
some content here
</div>
</div>
const eventContract = new jsaction.EventContract();
// Events will be handled for all elements under this container.
eventContract.addContainer(document.getElementById('container'));
// Register the event types we care about.
eventContract.addEvent('click');
eventContract.addEvent('dblclick');
const dispatcher = new jsaction.Dispatcher();
eventContract.dispatchTo(dispatcher.dispatch.bind(dispatcher));
/**
* Do stuff when actions happen.
* @param {!jsaction.ActionFlow} flow Contains the data related to the action
* and more. See actionflow.js.
*/
const doStuff = function(flow) {
// do stuff
alert('doStuff called!');
};
dispatcher.registerHandlers(
'leftNav', // the namespace
null, // handler object
{ // action map
'clickAction' : doStuff,
'doubleClickAction' : doStuff
});
JsAction splits the event contract and dispatcher into two separably loadable binaries. This allows applications to load the small event contract early on the page to capture events, and load the dispatcher and event handlers at a later time. Since captured events are queued until the dispatcher loads, this pattern can ensure that user events are not lost even if they happen before the primary event handlers load.
Visit http://jsfiddle.net/880m0tpd/4/ to try out a working example.
Just like in the regular example, in this example the event contract is loaded very early on the page, ideally in the head of the page.
<script id="contract" src="https://www.gstatic.com/jsaction/contract.js"></script>
<script>
const eventContract = new jsaction.EventContract();
// Events will be handled for all elements on the page.
eventContract.addContainer(window.document.documentElement);
// Register the event types handled by JsAction.
eventContract.addEvent('click');
</script>
<button jsaction="button.handleEvent">
click here to capture events
</button>
The event contract is configured to capture events for the entire page. Since the dispatcher and event handlers are not loaded yet, the event contract will just queue the events if the user tries to interact with the page. These events can then be replayed after the dispatcher and event handlers are loaded, which will be shown in this example next. This will ensure that no user interaction is lost, even if it happens before the code is loaded.
At any point later in the page, the dispatcher and event handlers can be loaded and any queued events can be replayed.
After the dispatcher and event handler code loads, you will configure the dispatcher just like in the regular example:
// This is the actual event handler code.
function handleEvent() {
alert('event handled!');
}
// Initialize the dispatcher, register the handlers, and then replay the queued events.
const dispatcher = new jsaction.Dispatcher();
eventContract.dispatchTo(dispatcher.dispatch.bind(dispatcher));
dispatcher.registerHandlers(
'button',
null,
{ 'handleEvent': handleEvent });
There is some new code to replay the queued events:
// This code replays the queued events. Applications can define custom replay
// strategies.
function replayEvents(events, jsActionDispatcher) {
while (events.length) {
jsActionDispatcher.dispatch(events.shift());
}
}
// This will automatically trigger the event replayer to run if there are
// queued events.
dispatcher.setEventReplayer(replayEvents);
Now any events that happen during page load before the JS has loaded will be replayed when the primary JS does load, ensuring that user interactions are not lost.