Isolate-Scope Attribute Expressions Use Dependency-Injection In AngularJS
When you create an isolate-scope in AngularJS directives, you have the ability to define attribute expressions that can be evaluated in the context of the parent scope. Until you start playing with these attribute expressions, it may not be entirely clear how they work. Not only do they use dependency-injection (DI), they also allow the isolate-scope directive to both provide and override DI "locals".
Run this demo in my JavaScript Demos project on GitHub.
When you see an isolate-scope attribute expression in your HTML, the order of the arguments is somewhat irrelevant. When AngularJS parses the expression, the resultant object is invoked using dependency-injection. And, while you can't inject "module" objects, you can inject scope objects based on name. And, whats more interesting is that this collection of parameters, used for the dependency-injection, can be populated by both the isolate-scope and the calling scope.
To see this in action, I've put together a small demo that defines an isolate scope that logs "click" events using an attribute expression. In the following code, look at the fact that when the expression is defined in the HTML, it can use either ordered arguments or an object hash; and yet, when it is invoked in the directive, the directive always uses a hash to define arguments.
<!doctype html>
<html ng-app="Demo">
<head>
<meta charset="utf-8" />
<title>
Isolate-Scope Attribute Expressions Use Dependency-Injection In AngularJS
</title>
<link rel="stylesheet" type="text/css" href="./demo.css"></link>
</head>
<body ng-controller="AppController">
<h1>
Isolate-Scope Attribute Expressions Use Dependency-Injection In AngularJS
</h1>
<ul class="friends">
<!--
When we consume the bnIsolate directive, we are providing a scope-method
that the isolate can evaluate on click events. The method arguments are
populated using AngularJS parsing and dependency-injection that uses
argument names to map inputs to existing scope values:
* friend - taken from local scope.
* $index - taken from local scope.
* linkedAt - scope override provided by isolate directive.
NOTE: Both the bn-isolate attributes "work" (when not disabled).
-->
<li
ng-repeat="friend in friends track by friend.id"
bn-isolate="logClick( friend, $index, linkedAt )"
DISABLED-bn-isolate="logClick( { linkedAt: linkedAt, index: $index, friend: friend } )"
class="friend">
{{ friend.id }} — {{ friend.name }}
</li>
</ul>
<!-- Load scripts. -->
<script type="text/javascript" src="../../vendor/jquery/jquery-2.1.0.min.js"></script>
<script type="text/javascript" src="../../vendor/angularjs/angular-1.2.26.min.js"></script>
<script type="text/javascript">
// Create an application module for our demo.
var app = angular.module( "Demo", [] );
// -------------------------------------------------- //
// -------------------------------------------------- //
// I control the root of the application.
app.controller(
"AppController",
function( $scope ) {
// I am the list of friends to render.
$scope.friends = [
{
id: 1,
name: "Sarah"
},
{
id: 2,
name: "Tricia"
},
{
id: 3,
name: "Joanna"
}
];
// ---
// PUBLIC METHODS.
// ---
// I log the click event on the friend in the list. This method can be
// invoked with two different signatures:
// --
// * logClick( friend, index, linkedAt )
// * logClick( { .. named arguments .. } );
$scope.logClick = function() {
// If the input is a hash, unwrap the inputs.
if ( arguments.length === 1 ) {
var friend = arguments[ 0 ].friend;
var index = arguments[ 0 ].index;
var linkedAt = arguments[ 0 ].linkedAt;
// If the inputs are isolated, get them from the arguments.
} else {
var friend = arguments[ 0 ];
var index = arguments[ 1 ];
var linkedAt = arguments[ 2 ];
}
console.info( friend.name, "[ index", index, " ] linked at", linkedAt );
};
}
);
// -------------------------------------------------- //
// -------------------------------------------------- //
// I create an isolate scope that takes an expression that will be evaluated on
// click events.
app.directive(
"bnIsolate",
function() {
// I bind the JavaScript events to the local scope.
function link( scope, element, attributes ) {
var linkedAt = ( new Date() ).getTime();
// When the user clicks, evaluate the click expression.
element.click(
function handleClick() {
// Since we're not in an AngularJS context, we have to let
// AngularJS know that the view-model may be changing. As
// such, we're doing this inside an $apply().
scope.$apply(
function updateViewModelmousedown() {
// When we invoke the isolate expression, the method
// does not rely on ORDERED parameters; rather, it
// uses dependency-injection that parses the method
// for NAMES and matches names to calling-context
// scope references. When we invoke this method, we
// have the opportunity to provide OVERRIDE values
// for the locals. In this case, we're padding in the
// linkedAt timestamp.
scope.click({
linkedAt: linkedAt
});
}
);
}
);
}
// Return the directive configuration. Notice that we are creating a
// isolate in which we're binding the "click" reference to an isolate
// scope attribute that can be evaluated in the calling context.
return({
link: link,
restrict: "A",
scope: {
click: "&bnIsolate"
}
});
}
);
</script>
</body>
</html>
As you can see, when the isolate-scope directive goes to invoke the attribute expression, it uses a hash to define a subset of the method arguments. The other arguments - friend and $index - are taken from the calling-context scope. In this way, the invocation arguments are a composite set of values taken from two different scopes.
This is a very powerful feature of isolate-scopes because it further decouples your directive from the parent application. While an isolate-scope and a calling context do have to agree on shared-argument names, the order and structure of the arguments are driven by the calling context (ie, the parent scope).
Want to use code from this post? Check out the license.
Reader Comments