Checking for defined functions in nested directives - javascript

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

Related

In Angular 1.x When "watching" in a directive; why does watching a function that returns a variable act different than just watching the variable?

The easiest way to explain this question is with some sample code so here is a very simple directive written in ES6 syntax:
export default class IsFoo {
constructor() {
// Set the directive properties
this.restrict = 'A';
this.require = 'ngModel';
}
link(scope, element, attributes, controller) {
let foo = scope.$eval(attributes.foo);
controller.$validators.isFooBar = (modelValue) => {
// make sure we have the most recent value foo
foo = attributes.foo;
return foo === 'bar';
};
scope.$watch(() => {return attributes.foo;}, () => controller.$validate());
}
static directiveFactory() {
IsFoo.instance = new IsFoo();
return IsFoo.instance;
}
}
IsFoo.directiveName = 'isFooBar';
That is a rough version of my directive with all the actual important validation removed.. it's pretty simple.
If I change the watch line to be:
scope.$watch(attributes.foo), ()=>controller.$validate());
It doesn't work. Why? Why does the function returning the attributes.foo work?
What is the difference that causes the end result to be different?
Also, disclaimer, I'm intentionally not using scope isolation because the directive is being used on an element that has another directive that uses scope isolation.. so they collide and you get an error Multiple directives asking for new/isolated scope on: xxx.
My rough guess is that it is related to how closures behave in javascript but I can't wrap my head around how the two behaving differently.
Thanks for any insight you can provide.
The interface for scope.$watch is according to the documentation the following:
$watch(watchExpression, listener, [objectEquality]);
With watchExpression being either a string or a function. If it is a string, it is interpreted as a path within your scope object. Assuming attributes.foo is "test.something", it will watch scope.test.something - if it exists.
If you want to watch for changes of the value of attributes.foo, you have to use the function, or attach attributes.foo to your scope and passing "attributes.foo" as watchExpression.

Angular - Passing an object from one directive to another directive

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.

add watch on a non scope variable in angularjs

Is there a way to add watch to a non scope variable. I want to add a watch to local variable. I have something like this
function EditAssetRegistryController(assetregistryService, manufacturerService, assettypeService, projectService, $localStorage, $routeParams) {
var vm = this;
vm.manufacturers = [];
vm.projects = [];
vm.asset_types = [];
vm.ch_group_uniq = 'none';
}
here is there a way to add watch to vm.ch_group_uniq?
I know how it will be done with scope variable but I have scenarios where I have to check many complex variables.
Well, you can easily add a watch for anything by passing a function as the first parameter:
$scope.$watch(function watchFunction(scope) {
return vm.ch_group_uniq
}, handler)
A few things to consider: watchFunction must return the same value if nothing has changed. This can lead to some gotchas, for example, returning the result of some array operations: [1,2,3].filter(...) will always return a new array, and lead to an endless $digest cycle. Also note the third parameter of $scope.$watch, which indicates whether to use an identity comparison, or angular.equals when comparing the values. (Check out the docs for further information - https://docs.angularjs.org/api/ng/type/$rootScope.Scope#$watch)
However, your specific problem seems to be trying to use controllerAs and custom $watch-es. There's a very handy library that addresses this issue specifically: https://github.com/christopherthielen/angular-360-no-scope
$watch will not work as normal syntax with controllerAs. You need to bind it to $scope, and then you can watch that variable:
Code
$scope.$watch(angular.bind(this, function (ch_group_uniq) {
return this.ch_group_uniq;
}), function (newVal, oldVal) {
console.log('Name changed to ' + newVal);
});
Here is the reference Todd Motto Article
A cleaner syntax using ES6
$scope.$watch(() => {
return this.thingToWatch;
}, (newVal, oldVal) => {
// Your code here...
});

Directive at angularjs and custom method/html

I have this code:
<body ng-controller="testController">
<div test-directive transform="transform()">
</div>
<script type="text/ng-template" id="testDirective.html">
<div>
<p>
{{transform()}}
</p>
</div>
</script>
<script>
angular.module("Test", [])
.directive("testDirective", function() {
return {
templateUrl: "testDirective.html",
scope: {
transform: "&"
},
link: function(scope) {
}
};
})
.controller("testController", function($scope) {
$scope.transform = function() {
return "<a ng-click='somethingInController()'>Do Something</a>";
};
$scope.somethingInController = function() {
alert("Good!");
};
});
</script>
</body>
So basically what I want to accomplish is to create a directive with a method that will be called from the controller. And that method will do something with the values passed (in this example it does not receives nothing, but in the real code it does).
Up to that point is working. However, the next thing I want to do is create an element that will call a method in the controller. The directive does not knows what kind of element will be (can be anything) nor what method will be. Is there any way to do it?
Fiddle Example:
http://jsfiddle.net/abrahamsustaita/C57Ft/0/ - Version 0
http://jsfiddle.net/abrahamsustaita/C57Ft/1/ - Version 1
FIDDLE EXAMPLE WORKING
http://jsfiddle.net/abrahamsustaita/C57Ft/2/ - Version 2
The version 2 is now working (I'm not sure if this is the way to go, but it works...). However, I cannot execute the method in the parent controller.
Yes. However there is a few problems with your code. I will start by answering your question.
<test-directive transform='mycustommethod'></test-directive>
// transform in the directive scope will point to mycustommethod
angular.module('app').directive('testDirective', function() {
return {
restrict: 'E',
scope: {
transform: '&'
}
}
});
The problem is that printing the html will be escaped and you will get < instead of < (etc.). You can use ng-bind-html instead but the returned html will not be bound. You will need to inject the html manually (you can use jquery for this) in your link method and use var compiled = $compile(html)(scope) to bind the result. Then call ele.after(compiled) or ele.replace(compiled) to add it to your page.
I finally get to get it working.
The solution is combined. First of all, I needed to add another directive to parse the element I wanted:
.directive("tableAppendElement", function ($compile) {
return {
restrict: "E",
replace: true,
link: function(scope, element, attrs) {
var el = angular.element("<span />");
el.append(attrs.element);
$compile(el)(scope);
element.append(el);
}
}
})
This will receive the element/text that will be appended and then will registered it to the scope.
However, the problem still exists. How to access the scope of the controller? Since my directive will be used by a lot of controllers, and will depend on the model of the controller, then I just set scope: false. And with that, every method in the controller is now accessible from the directive :D
See the fiddle working here. This also helped me because now, there is no need to pass the transform method, so the controller can be the one handling that as well.

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