Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / HTML5

Reducing Events and Binding in Angular using the Directive iv-on-cmd

0.00/5 (No votes)
20 Jan 2015MIT5 min read 6.3K  
The directive iv-on-cmd can be used to simplify handling Angular click events.

Introduction

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.

Background

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:

HTML
<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:

JavaScript
function startProcess() {
  // Do some kind of process here
}

function cancel() {
  // Cancel the operation here
}

$(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:

HTML
<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:

JavaScript
angular.module("mine").controller("myCtrl", myCtrl);
myCtrl.$inject = ["$scope"];
function myCtrl($scope) {
  $scope.startProcess = function() {
    // Do some kind of process here
  };

  $scope.cancel = function() {
    // Cancel the operation here
  }
}

But this could still, with hundreds of ng-click directives, cause a massive number of bindings to occur.

Using the Code

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:

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:

JavaScript
function processCmd(event) {
  var $el = $(event.target);
  var cmd = $el.data("cmd");

  console.log("Command was %s", cmd); 

  // Process the commands.
  switch(cmd) {
    case "start-process":
      // Do something
      break;

    case "cancel":
      // Do something
      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. But only for buttons that have the attribute data-cmd. 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.

HTML
<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.

JavaScript
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);
});

The Angular Directive: iv-on-cmd

My Angular directive, iv-on-cmd, uses the delegate functionality of jQuery to simplify and reduce the amount of Angular code needed. It does some 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.

HTML
<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.

JavaScript
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:

HTML
<button data-cmd="sayHello">Say Hello</button>

The object $event.data will be:

JavaScript
{
  "cmd": "sayHello",
  "cmdData": undefined
}

For this button:

HTML
<button data-cmd="speak" data-cmd-data="Hi">Say Hi</button>

The object $event.data will be:

JavaScript
{
  "cmd": "speak",
  "cmdData": "Hi"
}

You can also include objects in the data-cmd-data attribute.

For this button:

HTML
<button data-cmd="buy" 
data-cmd-data='{"title":"Test Product", "price": 3.95}'>Buy Now</button>

The object $event.data will be:

HTML
{
  "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.

This is the menu and toolbar from the project:

Menu and toolbar

This is the menu with a sub-menu open:

Menu and toolbar

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:

HTML
<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.

JavaScript
angular.module("mine").factory("myService", myService);
function myService() {
  return {
    "undo": undoFn,    
    "redo": redoFn,    
    "cut": cutFn,    
    "copy": copyFn,    
    "paste": pasteFn    
  }

  function undoFn() {
    // Do something
  }

  function redoFn() {
    // Do something
  }

  function cutFn() {
    // Do something
  }

  function copyFn() {
    // Do something
  }

  function pasteFn() {
    // Do something
  }
}

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)) { // See is the service supports the command
      myService[cmd]($event.data.cmdData); // $event.data.cmd will default to undefined
    }
    else {
      // Display an error or throw an exception
      // The cmd is not supported in the service 
    }
  }
}

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.

Points of Interest

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.

HTML
<script src="jquery_min.js"></script>
<script src="angular_min.js"></script>

Source Code

The source code is available as part of a Github project: angular-tools project

You can also go directly to the iv-on-cmd directive.

License

This article, along with any associated source code and files, is licensed under The MIT License