Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at the New York ColdFusion User Group (Jun. 2010) with:

Using jQuery Event Delegation In AngularJS

By Ben Nadel on

AngularJS is a very different beast when compared to jQuery (or vanilla JavaScript). In AngularJS, "context" is truly a first-class citizen. By that, I mean that AngularJS executes every piece of code in a specific container that is limited in scope. Unlike binding normal onclick event handlers, which have the entire document as a context, an ngClick event handler has a small, localized scope as its context. This event-containment requires a change in the way you think about binding event handlers; and, it challenges you to embrace code that "appears" to use an approach you've likely spent the last 5 years trying to get rid of. That said, I see people continue to ask about event-delegation in an AngualrJS application; so, I figured I'd give it whirl and see what I could come up with.


 
 
 

 
  
 
 
 

Because AngularJS uses a declarative approach to templating, I tried to come up with an attribute-based approach to defining the event-delegation configuration. And, since event delegation uses both a CSS selector and an event handler, I borrowed the "filter" syntax to pipe the selector into the handler expression:

bn-delegate="selector | handler"

In my simple experiment, the selector is optional; and, if omitted, it will default to "a" [anchor links]. Also, notice that I don't have an event type defined. For this experiment, I'm using "click" events only.

In the following experiment, I'm going to output a list of friend. Each of the friends renders a link, which when clicked, will show the detail information for that specific friend. The event delegation is defined on the root Unordered List; and, it pipes the event into the localized, contextual event handler, selectFriend().

  • <!doctype html>
  • <html ng-app="Demo" ng-controller="DemoController">
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Using jQuery Event Delegation In AngularJS
  • </title>
  • </head>
  • <body>
  •  
  • <h1>
  • Using jQuery Event Delegation In AngularJS
  • </h1>
  •  
  •  
  • <!--
  • List out all of the friends.
  •  
  • *****
  •  
  • NOTE: Add ng-controller="ListController" to see how event
  • delegation uses the most local, contextual scope when
  • invoking the event handler. The ListController will override
  • the selectFriend() defined on the root DemoController.
  • -->
  • <ul bn-delegate="li a | selectFriend( friend )">
  •  
  • <li ng-repeat="friend in friends">
  •  
  • <!-- Delegate target. -->
  • <a href="#">{{ friend.name }}</a>
  • <!-- Delegate target. -->
  •  
  • </li>
  •  
  • </ul>
  •  
  •  
  • <!-- IF a friend has been selected, show them. -->
  • <div ng-show="selectedFriend">
  •  
  • <p>
  • You selected:
  • {{ selectedFriend.id }} : {{ selectedFriend.name }}
  • </p>
  •  
  • </div>
  •  
  •  
  •  
  • <!-- Load jQuery and AngularJS from the CDN. -->
  • <script
  • type="text/javascript"
  • src="//code.jquery.com/jquery-1.9.0.min.js">
  • </script>
  • <script
  • type="text/javascript"
  • src="//ajax.googleapis.com/ajax/libs/angularjs/1.0.4/angular.min.js">
  • </script>
  •  
  • <!-- Load the app module and its classes. -->
  • <script type="text/javascript">
  •  
  •  
  • // Define our AngularJS application module.
  • var demo = angular.module( "Demo", [] );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I am the main controller for the application.
  • demo.controller(
  • "DemoController",
  • function( $scope ) {
  •  
  •  
  • // I select the given friend for display.
  • $scope.selectFriend = function( friend ) {
  •  
  • $scope.selectedFriend = friend;
  •  
  • };
  •  
  •  
  • // I am the list of friends to show.
  • $scope.friends = [
  • {
  • id: 1,
  • name: "Sarah"
  • },
  • {
  • id: 2,
  • name: "Franzi"
  • },
  • {
  • id: 3,
  • name: "Anna"
  • }
  • ];
  •  
  • // I determine which friend (if any) has been selected
  • // for display.
  • $scope.selectedFriend = null;
  •  
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I am the list controller.
  • demo.controller(
  • "ListController",
  • function( $scope ) {
  •  
  •  
  • // To demonstrate that the event delegation is
  • // happening in a local context, we can use this
  • // controller and method to override the selection
  • // of the friend.
  • $scope.selectFriend = function() {
  •  
  • // Choose a random friend index.
  • var randomIndex = Math.floor( Math.random() * $scope.friends.length );
  •  
  • // Invoke the parent select function with the
  • // randomly selected friend.
  • $scope.$parent.selectFriend(
  • $scope.friends[ randomIndex ]
  • );
  •  
  • }
  •  
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------------------- //
  • // -------------------------------------------------- //
  •  
  •  
  • // I define a click handler on the given element that will
  • // execute a statement in scope most local to the originating
  • // event target.
  • demo.directive(
  • "bnDelegate",
  • function( $parse ) {
  •  
  •  
  • // I bind the DOM and event handlers to the scope.
  • function link( $scope, element, attributes ) {
  •  
  •  
  • // Right now, the delegate can be defined as
  • // either selector and an expression; or simply
  • // as an expression.
  • var config = attributes.bnDelegate.split( "|" );
  •  
  • // Only an expression has been defined - default
  • // the selector to any anchor link.
  • if ( config.length === 1 ) {
  •  
  • var selector = "a";
  • var expression = config[ 0 ];
  •  
  • // Both selector and expression are defined.
  • } else {
  •  
  • var selector = config[ 0 ];
  • var expression = config[ 1 ];
  •  
  • }
  •  
  • // Parse the expression into an invokable
  • // function. This way, we don't have to re-parse
  • // it every time the event handler is triggered.
  • var expressionHandler = $parse( expression );
  •  
  •  
  • // -------------------------------------- //
  • // -------------------------------------- //
  •  
  •  
  • // Bind to the click (currently only supported
  • // event type) to the root element and listen for
  • // clicks on the given selector.
  • element.on(
  • "click.bnDelegate",
  • selector,
  • function( event ) {
  •  
  • // Prevent the default behavior - this is
  • // not a "real" link.
  • event.preventDefault();
  •  
  • // Find the scope most local to the target
  • // of the click event.
  • var localScope = $( event.target ).scope();
  •  
  • // Invoke the expression in the local scope
  • // context to make sure we adhere to the
  • // proper scope chain prototypal inheritance.
  • localScope.$apply(
  • function() {
  •  
  • expressionHandler( localScope );
  •  
  • }
  • );
  •  
  • }
  • );
  •  
  •  
  • // -------------------------------------- //
  • // -------------------------------------- //
  •  
  •  
  • // When the scope is destroyed, clean up.
  • $scope.$on(
  • "$destroy",
  • function( event ) {
  •  
  • element.off( "click.bnDelegate" );
  •  
  • }
  • );
  •  
  •  
  • }
  •  
  •  
  • // Return the directive configuration.
  • return({
  • link: link,
  • restrict: "A"
  • });
  •  
  • }
  • );
  •  
  •  
  • </script>
  •  
  • </body>
  • </html>

Notice that the we are using the jQuery on() method to delegate the click at the root of the list. When the list receives a click event, it gets the $scope most local to the event target by using the .scope() jQuery plugin. Then, it executes the delegated event handler in the context of that scope. In essence, this executes the click handler as if ngClick were defined on each anchor tag within the list.

In the demo code, I'm not using the ng-controller="ListController". However, if you look at the video, you can see how adding this controller overrides the behavior of the selectFriend() $scope method. I have this in there simply to demonstrate that the event delegation uses the $scope instance most local to the event target.

Honestly, I really enjoy the way AngularJS uses context. And, I really have come to appreciate how ngClick handlers work in a specific, localized context. But, if you have a case where event delegation seems warranted, it's still fairly easy to wrap it up in an AngularJS directive and use it. AngularJS is pretty darn flexible, even in its strict separation of Views and Controllers.

Tweet This Interesting post by @BenNadel - Using jQuery Event Delegation In AngularJS Thanks my man — you rock the party that rocks the body!



Reader Comments

Brillant! Truly reveals the inner logic of angular and how to articulate your classic js knowledge with this sharp framework.

I too wanted to use delegation for obvious performance reasons on a table with inline editing. However I read somewhere that ng-clicks on each row doesn't hamper performance as expected. Nevertheless if you multiply rows by the number of columns it does make quit a lot of DOM listeners.

Discovering the scope plugin was exactly what I was trying to fiddle with.

Hope you keep on that track of more advanced tutorials, they're great.

Reply to this Comment

@Charly,

I'm, really glad you liked the post. I too have found that a number of event bindings does not seem to be much of a problem; but, having event delegation "in the bag", so to speak, is definitely a good fallback.

The .scope() plugin is pretty awesome, when you need it. Take a look at this post:

http://www.bennadel.com/blog/2457-Accessing-scope-On-The-DOM-Using-AngularJS.htm

... I use it to easily update an AngularJS model based on the result of a jQuery UI sort. I've done the same thing in the past using start/end indices and splice()'ing values around... but using the DOM as your source of elements can be a huge efficiency.

Reply to this Comment

This is really good stuff, Ben!

I've enjoyed reading the small AngularJS pieces you have been putting together, they are really helpful. Keep up the good work!

Reply to this Comment

@Ron,

Thanks a lot! I'll try to keep it going. AngularJS has a steep learning curve; so I'm sure there will be not shortage of posts worth writing :)

Reply to this Comment

Another great share Ben ... Thanks ...

Re: angular delegates and the current scope ...

What are the benefits of using a delegate function over using the native angular 'Scope as Data-Model
' support.

<!--- I don't actually 'know' angular ... sue me. ;-) --->

<div ng-controller="FriendController">

<ul ng-something>

<li ng-repeat="friend in friends">

<!-- Delegate target. -->
<eh href="" ng-model="friend.name">{{ friend.name }}</a>


</li>

</ul>

</div>

Reply to this Comment

@Edward,

I think the only real benefit would be performance for large data sets. At the end of the day, AngularJS is stil creating event bindings, linking DOM elements to Controllers - it's just that it happens behind the scenes.

That said, I haven't found event performance to be an issue in any of my interfaces yet :)

Reply to this Comment

Hi,
Your example very helpful. Could you explain how to link two pages with your example? For e.g. Names are on one page and display information on next page.
Also how to set the information of 1st friend as default value without clicking.

Reply to this Comment

Great post, and addresses exactly what I was concerned with: the apparent potential performance impact of having ng-click within an ng-repeat where there are a lot of iterations. But the question is whether simple repeated ng-clicks actually do end up being a performance problem, and when you posted this, you seemed not quite sure. Now, over a year and 1/2 later, have you found cases where it was important to use this technique vs. plain-old ng-click?

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.