Angular - Passing an object from one directive to another directive - javascript

I am new to angular so apologies up front if a question is too newbie. I am trying to make a custom directive, and since I am already using an angular-youtube-embed directive, inside my new directive, I need to pass a player object from youtube-video directive, to my new directive, for the function playVideo in my scope to use it. I wonder how to do that?
This is how my directive looks:
angular.module('coop.directives')
.directive('youtubePlayer', function () {
return {
restrict: 'E',
scope: {
videoPlaying: '=videoPlaying',
playVideo: '&playVideo',
playerVars: '=playerVars',
article: '=article'
},
templateUrl : 'templates/youtube-player.html'
};
});
This is my youtube-player.html:
<img ng-hide='videoPlaying' ng-src='http://i1.ytimg.com/vi/{{ article.external_media[0].video_id }}/maxresdefault.jpg' class='cover'>
<youtube-video ng-if='videoPlaying' video-url='article.external_media[0].original_url' player='player' player-vars='playerVars' class='video'></youtube-video>
<div ng-hide='videoPlaying' class='iframe-overlay' ng-click='playVideo({player: player})'>
<img ng-hide='videoPlaying' class='play' src='icons/play.svg'/>
<img ng-hide='videoPlaying' class='playButton' src='icons/playRectangle.svg'/>
</div>
And this is the function from the controller that I would like to use in my directive:
$scope.playVideo = function(player) {
$scope.videoPlaying = true;
player.playVideo();
};
Where player is an object of youtube-video directive that I am using from angular-youtube-embed package.
So, whenever a user clicks on an element below, $scope.videoPlaying should become true and a playVideo() function should start the video:
<div ng-hide='videoPlaying' class='iframe-overlay' ng-click='playVideo(player)'>
This is how I call my directive in the view:
<youtube-player video-playing="videoPlaying" play-video="playVideo()" player-vars="playerVars" article="article"></youtube-player>
I should somehow pass a player object from youtube video to my new directive because now I get an error of:
ionic.bundle.js:26794 TypeError: Cannot read property 'playVideo' of
undefined:

You can use $broadcast to achieve this.
Below is the diagram explaining the concept.
In youtubePlayer Directive use broadcast -
$rootscope.$broadcast('player-object', $scope.player);
And receive it in your custom directive.
$scope.$on('player-object', function (event, player) {
$scope.videoPlaying = true;
player.playVideo();
});
Sample Example -http://jsfiddle.net/HB7LU/10364/

You can use '&' type for passing function in directives:
angular.module('coop.directives')
.directive('youtubePlayer', function () {
return {
restrict: 'E',
scope: {
action: '&', //<- this type of parameter lets pass function to directives
videoPlaying: '#videoPlaying',
...
so you directive will accept a parameter as a function, like this:
<coop.directives action="playVideo" videoPlaying="video" ...> </coop.directives>
and you'll be able to call that function normally:
article: '=article'
},
template : "<img ng-hide='videoPlaying' ng-src='http://i1.ytimg.com/vi/{{ article.external_media[0].video_id }}/maxresdefault.jpg' class='cover'><youtube-video ng-if='videoPlaying' video-url='article.external_media[0].original_url' player='player' player-vars='playerVars' class='video'></youtube-video><div ng-hide='videoPlaying' class='iframe-overlay' ng-click='playVideo(player)'><img ng-hide='videoPlaying' class='play' src='icons/play.svg'/><img ng-hide='videoPlaying' class='playButton' src='icons/playRectangle.svg'/></div>",
link: function (scope, element) {
scope.action();
}
Edit 1:
If none of those suggestions works, you can try to add () brackets to you action parameter action="playVideo()" or use '=' type parameter (but this way, your function will be double binded. In most cases you don't have to worry about it for functions, anyway).
You can find some examples in this old post: just try either solutions and find which one is working for your case.

Change the prefixes like this
#videoPlaying to =videoPlaying and
#playVideo to &playVideo
The # before variables is evaluated as string values by angular and you need to use two-way-binding in this case.

First of all, your question is contradicting. In your youtube-player.html, you use playVideo({player: player})
<div ng-hide='videoPlaying' class='iframe-overlay' ng-click='playVideo({player: player})'>
and just below that you say you use it as playVideo(player).
<div ng-hide='videoPlaying' class='iframe-overlay' ng-click='playVideo(player)'>
Assuming it is the second version, the problem here might be that the player reference actually is undefined and hence the youtube-video directive tries to assign values to an object that is not available. In order to solve this, assign an empty object to player in your youtube-player directive's controller.
angular.module('coop.directives').directive('youtubePlayer', function () {
return {
restrict: 'E',
scope: {
videoPlaying: '=videoPlaying',
playVideo: '&playVideo',
playerVars: '=playerVars',
article: '=article'
},
templateUrl : 'templates/youtube-player.html',
controller: function($scope) {
$scope.player = {};
}
};
});

Look at your button in your directive:
<div ng-hide='videoPlaying' class='iframe-overlay' ng-click='playVideo({player: player})'>
You are not passing player to the function, you are actually passing player as the value of a property on an object that you are creating within the function call: {player: player}
So when you go to call the function .playVideo() on the player object, you are actually trying to call it on the object you created in the function call: {player: player} which obviously doesn't have a function in it.
To fix it, you need to either change your function, or change the player object being passing into the function. Instead of this:
$scope.playVideo = function(player) {
$scope.videoPlaying = true;
player.playVideo();
};
You would need to change it to this:
$scope.playVideo = function(player) {
$scope.videoPlaying = true;
player.player.playVideo();
};
Or, alternatively, leave the function alone and change the object you are passing in:
<div ng-hide='videoPlaying' class='iframe-overlay' ng-click='playVideo(player)'>
JSFiddle
I've also created a JSFiddle showing the general concept of how your directive should be working.

easiest will be use $rootScope in directive and assign player in rootscope then use it in controller.
or better approach will be using directive.
directive:
in action you will assign a function with parameter.
rootApp.directive('ListTemplate', function () {
return {
restrict: 'EA',
replace: true,
transclude: true,
scope: {
list: '=',
action: '='
},
template: ' <div ng-click="bindSelectedGuest(guest.guid)" class="ct-clearfix info" ng-repeat="guest in list track by $index" data-tag="{{activeUser.guestId}}" ng-class="{ active : guest.guid==activeUser.guestId}">' +
'<label class="col-md-6 col-lg-7 ct-pull-left" data-tag="{{action}}" title="{{guest.firstName}}">{{guest.firstName}}</label>' +
'<label class="col-md-6 col-lg-5 ct-pull-right"><span class="fr" ng-if="guest.mobile" title="{{guest.displayMobile}}">{{guest.displayMobile}}</span>' +
'<span class="fr" ng-if="!guest.mobile">{{"N/A"}}</span>' +
'</label>' +
'<div class="info" ng-show="list.length==0"><div class="detail_alert message">No Record found</div></div></div>',
link: function ($scope, e, a) {
$scope.$watch('list', function () {
//console.log(list);
});
}
}
});
controller:
you will capture function you defined in action(directive) here.
> $scope.bindSelectedGuest($scope.selectedGuest.guid);

You can create an angular service for that and use it anywhere in the project. This service contains all type of functionality that you need in multiple directives.

The best way to pass an object to an angular directive is by using the &.
From the Angular Docs:
The & binding allows a directive to trigger evaluation of an
expression in the context of the original scope, at a specific time.
Any legal expression is allowed, including an expression which
contains a function call
When you use &, angular compiles the string as an expression and sets the scope variable in your directive to a function that, when called, will evaluate the expression in the context of the directive's parent's scope.
I'm going to make a small change to your directive to help clarify my explanation.
angular.module('coop.directives')
.directive('youtubePlayer', function () {
return {
restrict: 'E',
scope: {
videoPlaying: '=videoPlaying',
foo: '&playVideo',
playerVars: '=playerVars',
article: '=article'
},
templateUrl : 'templates/youtube-player.html'
};
});
I changed the name of the directive scope variable from playVideo to foo. From here forward, playVideo is a property of the parent, while foo is the property bound by the & binding to a property of the directive. Hopefully the different names will make things more clear (they are, in fact, completely separate properties/methods.
In your case, the object you are trying to pass is a function. In this case, there are two options, both are subtly different and depend on how you want the consumer of the directive to use it.
Consider this usage:
<youtube-player video-playing="videoPlaying" foo="playVideo()" player-vars="playerVars" article="article"></youtube-player>
In this case, the expression is "playVideo()". The & directive will create a property in your directive scope called "foo" that is a function that, when called, evaluates that expression in the parent scope. In this case, evaluating this expression would result in the parent scope's playVideo method being invoked with no arguments.
In this usage, your directive can only call the parent scope's method as is. No parameters can be overridden or passed to the function.
So:
foo() -> parent.playVideo()
foo(123) -> parent.playVideo() argument ignored
foo({player: 'xyz'}) -> parent.playVideo() argument ignored
Probably the preferred method if your parent method (playVideo) does not take any arguments.
Now consider a small change to the expression:
<youtube-player video-playing="videoPlaying" foo="playVideo(player)" player-vars="playerVars" article="article"></youtube-player>
Note the introduction of the local variable "player" in the expression. The function that is created in the directive's scope will do exactly the same thing as in the previous example, but it can now be called in two different ways. The variable "player" is considered a local variable in the expression.
The function foo generated by angular takes an argument that allows the directive to override the value of local variables in an expression. If no override is provided, it looks for a property of the parent scope with that name, if no such property exists, it will pass undefined to the function. So in this case:
$scope.foo() -> parent.playVideo(parent.player)
$scope.foo(123) -> parent.playVideo(parent.player)
$scope.foo({player: 'xyz'}) -> parent.playVideo('xyz')
If you want to pass the player from the directive to the parent, this is a weird way to do it (IMHO), because you have to know the name of the local variable in the expression. That creates an unnecessary requirement that the directive and the expression agree on the name of the argument.
The final way the playVideo function could be bound is:
<youtube-player video-playing="videoPlaying" foo="playVideo" player-vars="playerVars" article="article"></youtube-player>
In this case, the expression, evaluated against the parent, returns the function playVideo of the parent. In the directive, to call the function, you then have to invoke it.
$scope.foo() -> noop (you now have a pointer to the parent.playVideo function
$scope.foo()() -> parent.playVideo()
$scope.foo()('xyz') -> parent.playVideo('xyz')
This last way, in my very humble opinion, is the proper way to pass a function pointer that takes an argument to a directive and use it within the directive.
There are some esoteric side effects that can be used (but shouldn't). For instance
$scope.foo({playVideo: function(){
alert('what????')
})();
This will not call the parent.playVideo function since you've overriden the expression's local variable "playVideo" with a custom version in the directive. Instead, it will pop up an alert dialog. Weird, but that's the way it works.
So, why not use # or =?
If you use #, you essentially have to do what & does manually in the directive. Why do that when & will do it for you? '=' actually sets up two way binding, allowing the directive to change the value of the parent's property (potentially changing the function itself!) and vice-versa. Not a desirable side effect. This two-way binding also requires two watches which essentially are doing nothing but taking up cpu cycles since you aren't likely using them to update UI elements.
I hope this helps clear things up.

Related

Force Two-Way-Binding to Take Effect

I am writing an AngularJS 1.x directive (let's call it MyDirective). Its scope is declared as follows:
scope: {
accessor: '='
}
In its link function, I am assigning a new object to that accessor field, like so:
scope.accessor = {
// methods such as doSomethingToMyDirective()
};
Now, I am instantiating this directive dynamically with $compile:
var element = $compile('<div data-my-directive data-accessor="directiveAccessor"></div>')(myScope);
Once this has run, my current scope (myScope) has a directiveAccessor property that references the object instance created within the directive.
Problem: This field is not immediately available.
In other words, once I have run $compile, I cannot access myScope.directiveAccessor immediately in the next command. When I check the scope later, the field is there, and probably, a single $timeout would be sufficient.
With some breakpoints, I can observe that the object is indeed created right when $compile is executed; accessor on the inner scope already points to the object. However, it seems that the two-way-binding that would copy the value from accessor on the inner scope to myScope.directiveAccessor does not become active until a later point.
Is there any way to force AngularJS to copy two-way-bound values immediately (i.e. without waiting for any promise)?
Use expression binding (&) to immediately set a parent scope variable:
app.directive("myDirective", function () {
return {
scope: { onPostLink: "&" },
link: postLink
};
function postLink(scope, elem, attrs) {
scope.accessor = {
doSomethingToMyDirective: function() {
return "Hello world";
}
};
scope.onPostLink({$event: scope.accessor});
scope.$on("$destroy", function() {
scope.onPostLink({$event: null});
});
}
})
Usage:
<my-directive on-post-link="directiveAccessor=$event">
</my-directive>
Be sure to null the reference when the isolate scope is destroyed. Otherwise the code risks creating memory leaks.

Checking for defined functions in nested directives

When passing a function into a directive which then is passed into a nested child directive the function is always considered defined when checked in the scope of the child directive regardless if it is passed in or not in the parent directive.
Is there a better way to either pass in function pointers or check if they are defined when dealing with nested directives.
plunker
<body ng-app="myApp">
<div ng-controller="myController">
<dir1"></dir1>
</div>
<script type="text/ng-template" id="dir1">
<div>
<dir2 fun="fun()"></dir2>
</div>
</script>
<script type="text/ng-template" id="dir2">
<div>{{fun()}}</div>
<div>{{funDefined()}}</div> <!-- always true-->
</script>
</body>
var app = angular.module('myApp', []);
app.controller('myController', function($scope) {
$scope.fun = function() {
alert("function");
};
});
app.directive('dir1', function() {
return {
scope: {
fun: '&'
},
templateUrl: 'dir1'
};
});
app.directive('dir2', function() {
return {
scope: {
fun: '&'
},
link: function(scope, elem, attrs) {
scope.funDefined = function() {
return angular.isDefined(attrs.fun);
};
},
templateUrl: 'dir2'
};
});
If you set debugger inside your scope.funDefined method of dir2 you'll see that attrs.fun equals string "fun()". That's because you take raw value from attributes. And since it's a not empty string it'll always give you true.
Here is updated plunker
There's no elegant way I know to get what you want. Like it was mentioned before this line:
angular.isDefined(attrs.fun)
performs check on string so it will return true every time fun attribute is defined. And in your dir1 directive template you have <dir2 fun="fun()"></dir2> so fun is obviously defined (and it's string). If you take a look at angular's sources:
case '&':
// Don't assign Object.prototype method to scope
parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop;
// Don't assign noop to destination if expression is not valid
if (parentGet === noop && optional) break;
destination[scopeName] = function(locals) {
return parentGet(scope, locals);
};
break;
you'll see that presence of the attribute will always result in some function assigned to the scope ($parse returns function even for string that doesn't make much sense).
So the only solution I can think of is to perform check in the first level directive (it's possible there since attribute is really undefined) and have two <dir2> tags (with and without fun attribute) - one always excluded using ng-if. Something like this. Again, I know, its ugly solution.
One side note - Angular's source also shows that scope property will not be set if there's no attribute and binding is optional (using &?) - then you can check scope.fun value instead of attrs.fun - some may find it more elegant.
The best way I could find is based in what #xersiee commented in another answer. The idea is to make the scope parameter optional in the parent directive and then use angular.isUndefined(scope.$parent.$eval(attribute.myFun)) to check if the function was passed or not. This is not explained in the official documentation... I wonder why.
As other people has mentioned, this solution is far from ideal because using scope.$parent is an anti-pattern, but again, this is the best option I could find.
Plunker with this solution: http://plnkr.co/edit/SUUMae?p=preview

when using Angular-google-chart directive, how do you access the selected item in the pie chart?

I'm using the angular-google-charts [bouil.github.io/angular-google-chart/] directive to create a pie chart.
I can fire a method using this with
<div google-chart chart="myChart" style="{{cssStyle}}" on-select="seriesSelected()"></div>
$scope.seriesSelected = function () {
console.log('item selected');
}
However, I can't work out how to get the key of the selected item. I can see how to do this when using google charts without the angular directive: how to stackoverflow answer. However, I can't follow how to get the element when using angular. This guy seems to have an answer (there is also a plunkr here that fairly accurately shows what I am trying to do), but it seems to be more complex than what I am looking for.
I can see in the ng-google-chart.js directive, there is a line, that adds a property to the selected items:
var selectEventRetParams = {selectedItems:$scope.chartWrapper.getChart().getSelection()};
but I'm not yet able to see how to access this property. Any advice is much appreciated.
Documentation
Just change the html to the following:
<div google-chart chart="myChart" style="{{cssStyle}}" agc-on-select="seriesSelected(selectedItem)"></div>
I couldn't access the directive scope either. So I added a new attribute to the isolated scope and set it "=".
The HTML:
<div google-chart chart="chartObject" style="{{cssStyle}}" custom-select="handleSelect"></div>
Modified directive scope:
scope: {
beforeDraw: '&',
chart: '=chart',
onReady: '&',
onSelect: '&',
select: '&',
customSelect: '='
},
Add this to the "select" listener:
if($attrs.customSelect){
$scope.customSelect(selectEventRetParams);
}
My event handler:
$scope.handleSelect=function(selection){
console.log(selection);
};
http://jsfiddle.net/s911131/sjh4wfe2/5/
Almost there... referring back to your code:
$scope.seriesSelected = function () {
console.log('item selected');
}
Should be changed to:
$scope.seriesSelected = function (selectedItem) {
console.log('item selected');
console.log(selectedItem);
}
In order to pick up the value as passed by the directive.
UPDATE:
This was a doozy. The parameter name 'selectedItem' used in the markup MUST match that being passed back from the directive's isolate scope!!
on-select="doThis(selectedItem)"
https://docs.angularjs.org/guide/directive does mention it, I didn't read properly.
"Often it's desirable to pass data from the isolate scope via an expression to the parent scope, this can be done by passing a map of local variable names and values into the expression wrapper fn. For example, the hideDialog function takes a message to display when the dialog is hidden. This is specified in the directive by calling close({message: 'closing for now'}). Then the local variable message will be available within the on-close expression."
ORIGINAL QUESTION:
#Sam - did you ever get this to work? I have set breakpoints both in angular-google-charts and my code and I can see a valid selectedItem variable being constructed and passed into $scope.onSelect({selectedItem: selectedItem}) -
google.visualization.events.addListener($scope.chartWrapper, 'select', function () {
var selectedItem = $scope.chartWrapper.getChart().getSelection()[0];
$scope.$apply(function () {
if ($attrs.select) {
console.log('Angular-Google-Chart: The \'select\' attribute is deprecated and will be removed in a future release. Please use \'onSelect\'.');
$scope.select({ selectedItem: selectedItem });
}
else{
$scope.onSelect({ selectedItem: selectedItem });
}
});
However by the time this reaches my code, the selItem parameter is undefined.
my controller code:
$scope.doThis = function(selItem){
alert("a");
};
my markup:
<div google-chart chart="chartObject" on-select="doThis(selItem)" style="{{cssStyle}}" ></div>
I"ve tried both Angular 1.2.x and 1.4.1 - same behavior in both.
#df1 - I can't see how your solution would work since you are calling a function $scope.customSelect(selectEventRetParams), but your directive's isolate scope has declared customSelect to be bound using '=' instead of '&' for expressions/function callbacks.
I want to improve my answer and to spend more time looking into others' answers. I have a working solution, which is as follows. Modify the directive scope by adding a two way binding called selectedItem:
scope: {
beforeDraw: '&',
chart: '=chart',
onReady: '&',
onSelect: '&',
select: '&',
selectedItem: "="
}
Then my function in the directive is as follows:
google.visualization.events.addListener($scope.chartWrapper, 'select', function (type) {
var selectEventRetParams = { selectedItems: $scope.chartWrapper.getChart().getSelection() };
selectEventRetParams['selectedItem'] = selectEventRetParams['selectedItems'][0];
$scope.selectedItem = selectEventRetParams['selectedItem'].row;
$scope.select({ selectEventRetParams: selectEventRetParams });
}
Then, I have a watch function in my own code, which happens to also be a directive with it's own controller and this code looks like this:
$scope.$watch('selectedItem', function (newValue) {
if (newValue != null) {
$scope.handleSelectedItem();
}
});
The HTML looks like this:
<div google-chart chart="chartObject" style="{{cssStyle}}" sselected-item="selectedItem"></div>
I have actually used several two way bindings and use this to click into the pie chart multiple times, diving into the data. It works really well, but I need to tidy my code somewhat and come back to this.

Is it safe to remove dependencies in the controller function when the directive does not seem to use them?

The following controller does not seem to use $element, $attrs, $transclude. The controller code below runs fine if these params are commented out.
myApp.directive("menu", function () {
return {
restrict: "E",
scope: {},
transclude: true,
replace: true,
template: "<div class='menu' data-ng-transclude></div>",
controller: function ($scope ,$element, $attrs, $transclude) {
$scope.submenus = [];
console.log('[$element]->', $element);
console.log('[$attrs]->', $attrs);
console.log('[$transclude]->', $transclude);
this.addSubmenu = function (submenu) {
console.log('[addToggleMenu]->');
$scope.submenus.push(submenu);
}
this.closeAllOtherPanes = function (displayedPane) {
angular.forEach($scope.submenus, function (submenu) {
if (submenu != displayedPane) {
console.log('[displayedPane]->', displayedPane);
submenu.removeDisplayClass();
}
})
}
}
}
});
Here is my working fiddle.
Since posting, I have learned that in JavaScript, a function can be called with any number of arguments no matter how many of them are listed. In some languages, a programmer may write two functions with same name but different parameter list, and the interpreter/compiler would choose the right one. That is called function polymorphism. Having used function polymorphism most of my career I expected to be told "Hey , you're not using this param". Also I did not understand that well that the controller parameters are dependencies while the link function parameters are order based. I still struggle in understanding whether $scope, $element or commonly used parameters in the directives internal controller are required and which are optional. Apparently the $ is only required in the controller and not in link because of the DI injection of angular services..whew lot to digest.
A special thanks to Esteban for explaining special pseudo-array inside each function
called arguments. This explains the link function which is half the equation. So I have rewritten the question in hopes that it may get answered. This excellent explanation,
straightened out most of my confusion.
Why are you wanting to remove the $attrs argument? The directive's arguments are in a specific order and removing any one of them will cause subsequent arguments to not be what you think they are.
Say you changed your link function to something like this:
function ($scope, $iElement, menuController) {
menuController.addSubmenu($scope); // this will throw an exception
// because menuController is actually
// $attrs
The reason for this is due to the fact that the function will be called with the same arguments passed in regardless of whether they were defined in your directive's link function.
function argTest(one, two){
console.log('argTest.arguments', arguments);
// even though I don't have three defined, it is still passed in:
console.log('three is passed in', arguments[2]);
}
argTest(1,2,3);
JSFiddle for above: http://jsfiddle.net/TwoToneBytes/NM8DK/

Using a directive inside an ng-repeat, and a mysterious power of scope '#'

If you prefer to see the question in working code, start here: http://jsbin.com/ayigub/2/edit
Consider this almost equivalent ways to write a simple direcive:
app.directive("drinkShortcut", function() {
return {
scope: { flavor: '#'},
template: '<div>{{flavor}}</div>'
};
});
app.directive("drinkLonghand", function() {
return {
scope: {},
template: '<div>{{flavor}}</div>',
link: function(scope, element, attrs) {
scope.flavor = attrs.flavor;
}
};
});
When used by themselves, the two directives work and behave identically:
<!-- This works -->
<div drink-shortcut flavor="blueberry"></div>
<hr/>
<!-- This works -->
<div drink-longhand flavor="strawberry"></div>
<hr/>
However, when used within an ng-repeat, only the shortcut version works:
<!-- Using the shortcut inside a repeat also works -->
<div ng-repeat="flav in ['cherry', 'grape']">
<div drink-shortcut flavor="{{flav}}"></div>
</div>
<hr/>
<!-- HOWEVER: using the longhand inside a repeat DOESN'T WORK -->
<div ng-repeat="flav in ['cherry', 'grape']">
<div drink-longhand flavor="{{flav}}"></div>
</div>
My questions are:
Why does the longhand version not work inside an ng-repeat?
How could you make the longhand version work inside an ng-repeat?
In drinkLonghand, you use the code
scope.flavor = attrs.flavor;
During the linking phase, interpolated attributes haven't yet been evaluated, so their values are undefined. (They work outside of the ng-repeat because in those instances you aren't using string interpolation; you're just passing in a regular ordinary string, e.g. "strawberry".) This is mentioned in the Directives developer guide, along with a method on Attributes that is not present in the API documentation called $observe:
Use $observe to observe the value changes of attributes that contain interpolation (e.g. src="{{bar}}"). Not only is this very efficient but it's also the only way to easily get the actual value because during the linking phase the interpolation hasn't been evaluated yet and so the value is at this time set to undefined.
So, to fix this problem, your drinkLonghand directive should look like this:
app.directive("drinkLonghand", function() {
return {
template: '<div>{{flavor}}</div>',
link: function(scope, element, attrs) {
attrs.$observe('flavor', function(flavor) {
scope.flavor = flavor;
});
}
};
});
However, the problem with this is that it doesn't use an isolate scope; thus, the line
scope.flavor = flavor;
has the potential to overwrite a pre-existing variable on the scope named flavor. Adding a blank isolate scope also doesn't work; this is because Angular attempts to interpolate the string on based on the directive's scope, upon which there is no attribute called flav. (You can test this by adding scope.flav = 'test'; above the call to attrs.$observe.)
Of course, you could fix this with an isolate scope definition like
scope: { flav: '#flavor' }
or by creating a non-isolate child scope
scope: true
or by not relying on a template with {{flavor}} and instead do some direct DOM manipulation like
attrs.$observe('flavor', function(flavor) {
element.text(flavor);
});
but that defeats the purpose of the exercise (e.g. it'd be easier to just use the drinkShortcut method). So, to make this directive work, we'll break out the $interpolate service to do the interpolation ourself on the directive's $parent scope:
app.directive("drinkLonghand", function($interpolate) {
return {
scope: {},
template: '<div>{{flavor}}</div>',
link: function(scope, element, attrs) {
// element.attr('flavor') == '{{flav}}'
// `flav` is defined on `scope.$parent` from the ng-repeat
var fn = $interpolate(element.attr('flavor'));
scope.flavor = fn(scope.$parent);
}
};
});
Of course, this only works for the initial value of scope.$parent.flav; if the value is able to change, you'd have to use $watch and reevaluate the result of the interpolate function fn (I'm not positive off the top of my head how you'd know what to $watch; you might just have to pass in a function). scope: { flavor: '#' } is a nice shortcut to avoid having to manage all this complexity.
[Update]
To answer the question from the comments:
How is the shortcut method solving this problem behind the scenes? Is it using the $interpolate service as you did, or is it doing something else?
I wasn't sure about this, so I looked in the source. I found the following in compile.js:
forEach(newIsolateScopeDirective.scope, function(definiton, scopeName) {
var match = definiton.match(LOCAL_REGEXP) || [],
attrName = match[2]|| scopeName,
mode = match[1], // #, =, or &
lastValue,
parentGet, parentSet;
switch (mode) {
case '#': {
attrs.$observe(attrName, function(value) {
scope[scopeName] = value;
});
attrs.$$observers[attrName].$$scope = parentScope;
break;
}
So it seems that attrs.$observe can be told internally to use a different scope than the current one to base the attribute observation on (the next to last line, above the break). While it may be tempting to use this yourself, keep in mind that anything with the double-dollar $$ prefix should be considered private to Angular's private API, and is subject to change without warning (not to mention you get this for free anyway when using the # mode).

Categories

Resources