iv-on-cmd
is an Angular directive that allows for a single event handler to handle click
events for all child elements of the element containing the directive iv-on-cmd
as long as the child elements have the attribute data-cmd
set.
The goal of iv-on-cmd
is to reduce the number of ng-click
handlers and provide a single common function to handle all of the children.
Backstory
I have discovered that Angular tends to slow down with too many bindings and I have read articles that indicate that as few as two-thousand bindings can be too many bindings.
I have had several projects where the result of an ng-repeat
created several hundred to several thousand DOM elements. And each of these elements had between three and thirty bindings. This was way more than the estimated slow-down limit of two thousand.
I needed a way to do something I used to do with jQuery. And that is use a single click handler on a wrapper <div>
that tells me the command to execute for all of the wrapper's children. Using jQuery, I would do the following:
<div class="main-wrapper">
<button class="start-process">Start</button>
<button class="cancel">cancel</button>
</div>
The above is a greatly simplified DOM, but it works for this explanation.
With the example HTML above, I would add some jQuery to process each <button>
independently:
function startProcess() {
}
function cancel() {
}
$(document).ready(function() {
$(".start-process").on("click", startProcessFn);
$(".cancel").on("click", cancelFn);
});
But when I had hundreds of these buttons, or other DOM elements, it got to be a massive process writing each of these click
handlers.
Angular helped improve this by allowing you to set the click handler via the ng-click
directive, like this:
<div class="main-wrapper" data-ng-controller="myCtrl">
<button data-ng-click="startProcess()">Start</button>
<button data-ng-click="cancel()">cancel</button>
</div>
Then in the controller code, I would do this:
angular.module("mine").controller("myCtrl", myCtrl);
myCtrl.$inject = ["$scope"];
function myCtrl($scope) {
$scope.startProcess = function() {
};
$scope.cancel = function() {
}}
But this could still, with hundreds of ng-click
directives, cause a massive number of bindings to occur.
My jQuery Solution
To resolve the many event bindings in jQuery, I create a command handler. This would utilize the delegate
version of the $().on()
function. This is done by setting the $().on()
handler on a parent and specify the children that will cause your code to be called. So with this HTML:
<div class="main-wrapper">
<button data-cmd="start-process">Start</button>
<button data-cmd="cancel">cancel</button>
</div>
And the script would look like this:
function processCmd(event) {
var $el = $(event.target);
var cmd = $el.data("cmd");
console.log("Command was %s", cmd);
switch(cmd) {
case "start-process":
break;
case "cancel":
break;
}
}
$(document).ready(function() {
$(".main-wrapper").on("click", "[data-cmd]", processCmd);
});
With this code, the click
handler is a delegated handler. Meaning that when the user clicks on the <button>
, the event is delegated to the event handler connected to the <div>
tag. Now, even with hundreds of buttons, I only have one event handler. And, if buttons are added later, my event handler is still called.
The example above is small enough that what I am describing may not make sense. But imagine having something that is repeated and the only difference between each of them is an index value or some form of a key value. Take a web-based message application as an example. Each message has its own unique identifier. If each message had a read button and a delete button, then you would need two event handlers per message. But using the delegate for of $().on()
, we can have one event handler that handles all of the messages.
<div class="mail-shell">
<div class="message">
<span class="sender">someone@example.com</span>
<span class="subject">Some message subject</span>
<span class="time">3:43 am</span>
<span><button data-cmd="read" data-cmd-data="KE1R-DJ5KW-9SJ21">Read</button></span>
<span><button data-cmd="delete" data-cmd-data="KE1R-DJ5KW-9SJ21">Delete</button></span>
</div>
<div class="message">
<span class="sender">person@example.com</span>
<span class="subject">Buy something from us</span>
<span class="time">2:49 am</span>
<span><button data-cmd="read" data-cmd-data="K19D-0PWR8-MMK92">Read</button></span>
<span><button data-cmd="delete" data-cmd-data="K19D-0PWR8-MMK92">Delete</button></span>
</div>
<div class="message">
<span class="sender">bot@example.com</span>
<span class="subject">Buy a Rolex from us</span>
<span class="time">2:31 am</span>
<span><button data-cmd="read"
data-cmd-data="LK0P-HN8GT-00LPD">Read</button></span>
<span><button data-cmd="delete" data-cmd-data="LK0P-HN8GT-00LPD">Delete</button></span>
</div></div>
Now imagine hundreds of these messages instead of the three in the example above.
Using a few lines of code and only one event handler, we can handle all of the click
events for all of the buttons, even if a new message shows up after we set up our event handler.
function processCmd(event) {
var $el = $(event.target);
var cmd = $el.data("cmd");
var cmdData = $el.data("cmdData");
switch(cmd) {
case "read":
openMessage(cmdData);
break;
case "delete":
deleteMessage(cmdData);
break;
}
}
$(document).ready(function() {
$(".main-wrapper").on("click", "[data-cmd]", processCmd);
});
My Angular Directive: iv-on-cmd
My Angular directive, iv-on-cmd
, used the delegate functionality of jQuery to simplify Angular code. It does some of the behind-the-scenes work for you. It figures out what the command cmd
is and the command data cmdData
and inserts that into the $event.data
object. Then it passes $event
through to your handler.
The following HTML example has the iv-on-cmd
directive on the outer <div>
. This allows one event handler processCmd()
to handle all of the click events from the three child buttons.
<div data-ng-controller="myCtrl" data-iv-on-cmd="processCmd($event)">
<button data-cmd="sayHello">Say Hello</button>
<button data-cmd="speak" data-cmd-data="Hi">Say Hi</button>
<button data-cmd="speak" data-cmd-data="Bye">Say Bye</button>
</div>
The example controller below supplies the processCmd()
function that is to be accessed any time the user clicks on one of the buttons with the data-cmd
attribute.
angular.module("mine").controller("myCtrl", myCtrl);
myCtrl.$inject = ["$scope"];
function myCtrl($scope) {
$scope.processCmd = function($event) {
$event.stopPropigation();
$event.preventDefault();
if ($event.data.cmd === "sayHello") {
alert("Hello");
return;
}
if ($event.data.cmd === "speak" ) {
alert("Speaking: " + $event.data.cmdData);
return;
}
}
}
In the buttons that have data-cmd="speak"
, the code will also use the attribute data-cmd-data
. This attribute value is read and placed into the $event.data
object along with the value from data-cmd
.
For this button:
<button data-cmd="sayHello">Say Hello</button>
The object $event.data
will be:
{
"cmd": "sayHello",
"cmdData": undefined
}
For this button:
<button data-cmd="speak" data-cmd-data="Hi">Say Hi</button>
The object $event.data
will be:
{
"cmd": "speak",
"cmdData": "Hi"
}
You can also include objects in the data-cmd-data
attribute.
For this button:
<button data-cmd="buy" data-cmd-data='{"title":"Test Product", "price": 3.95}'>Buy Now</button>
The object $event.data
will be:
{
"cmd": "buy",
"cmdData": {
"title": "Test Product",
"price": 3.95
}
}
Example: Menus and Toolbars
I had a project that had both a menu, with sub-menus, and a tool bar. Most of the menu items were replicated by the tool bar elements. So the user could perform the same operation using the menu or using a toolbar button.
Here is the toolbar:
And the menu:
And the sub-menu:
The menu was created using one directive and the toolbar was created using a second directive. But both directives just added the attribute data-cmd
to the DOM elements and did not process the click events (with the exception of the menu that would toggle the sub-menu open and closed.)
The menu and toolbar were contained within a single <div>
and it was on this <div>
that I added the directive iv-on-cmd
like below:
<div data-iv-on-cmd="processCmd($event)">
<ul class="menu">
<li>
<button data-ng-click="toggle('batch')">Batch</button>
<ul class="sub-menu" data-menu="batch">
<li>...</li>
...
</ul>
</li>
<li>
<button data-ng-click="toggle('image')">Image</button>
<ul class="sub-menu" data-menu="image">
<li><button data-cmd="ruler">Ruler</button></li>
<li><button data-cmd="highlights">Highlights</button></li>
</ul>
</li>
...
</ul>
<div class="toolbar">
<button class="toolbar__button" data-cmd="unto"><img src="img/undo.png"></button>
<button class="toolbar__button" data-cmd="redo"><img src="img/redo.png"></button>
<span class="toolbar__separator"></span>
<button class="toolbar__button" data-cmd="cut"><img src="img/cut.png"></button>
<button class="toolbar__button" data-cmd="copy"><img src="img/copy.png"></button>
<button class="toolbar__button" data-cmd="paste"><img src="img/paste.png"></button>
<span class="toolbar__separator"></span>
...
</div>
</div>
The controller provided a single function processCmd()
that would process each of the commands.
angular.module("mine").factory("myService", myService);
function myService() {
return {
"undo": undoFn,
"redo": redoFn,
"cut": cutFn,
"copy": copyFn,
"paste": pasteFn
};
function undoFn() {
}
function redoFn() {
}
function cutFn() {
}
function copyFn() {
}
function pasteFn() {
}
}
angular.module("mine").controller("myCtrl", myCtrl);
myCtrl.$inject = ["$scope", "myService"];
function myCtrl($scope, myService) {
$scope.processCmd = function($event) {
$event.stopPropigation();
$event.preventDefault();
var cmd = $event.data.cmd;
if (myService.hasOwnProperty(cmd)) {
myService[cmd]($event.data.cmdData);
}
else {
}
}
}
This is a very simple example. But it shows that we can generate simple HTML that supplies data-cmd
attributes. Then, with a single command handler, we can process those commands. In this example, I also moved the work of processing the commands off to a service. Though you may need to perform async operations or get data back from the service call which would change the way the code was written.
The Need for jQuery and Not jqLite
This directive requires you to load jQuery before loading Angular. It does not work with the jqLite found in Angular because jqLite does not support the delegate mode of the $().on()
function.
jQuery version 1.7 or greater is required because those support the delegate mode of the $().on()
function.
<script src="jquery_min.js"></script>
<script src="angular_min.js"></script>
Github Repo
The directive iv-on-cmd
is part of a set of tools I am maintaining at https://github.com/intervalia/angular-tools.
Questions...
I hope that I have provided enough examples to help show the value of the iv-on-cmd
directive. If you have questions, please ask them.
CodeProject