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.
Want to use code from this post? Check out the license.
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4