Force Two-Way-Binding to Take Effect - javascript

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.

Related

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.

Angular directive parent scope

I'm building a directive that decorates a table header item and sort the data upon the click event. I cannot apply the changes on the data model to the parent scope using the directive scope.
vm.data is an array on the parent scope that contains the data I want to sort in the directive.
After the click the data object in the directive has changed but the parent is still in the same order.
I dont want to access the parent scope using $parent, What I'm missing ??
<th sortable="browser" data="vm.data">Browser</th>
directive code:
angular
.module("app")
.directive("sortable", ['lodash', sortableDirective]);
function sortableDirective(lodash) {
return {
restrict: "A",
scope:{
data:"="
},
controller:function($scope){
},
link: function (scope, element, attributes) {
var sorted = undefined;
var col = attributes['sortable'];
var oldClass = 'sorting'
attributes.$$element.addClass(oldClass);
$(element).on("click", sort);
function changeClass(){
if(sorted=='asc'){
attributes.$$element.removeClass(oldClass);
attributes.$$element.addClass('sorting_asc');
oldClass = 'sorting_asc';
}
else if(sorted=='desc'){
attributes.$$element.removeClass(oldClass);
attributes.$$element.addClass('sorting_desc');
oldClass='sorting_desc';
}
}
function sort() {
if (sorted == 'asc') {
sorted = 'desc';
}
else {
sorted = 'asc';
}
scope.data = lodash.sortBy(scope.data, function (o) {
return o[col];
});
if (sorted == 'desc') {
lodash.reverse(scope.data);
}
changeClass();
}
}
};
}
This is because you are using jQuery to listen to change on the element. So just change this line:
$(element).on("click", sort);
to
element.on("click", sort);
The 2nd attribute i.e. element is already an instance of jQlite if jQuery is not available and will be an instance of jQuery if jQuery is available.
In any case, there is a method available .on which will be executed on the value change. Since you again wrapped it to $(), the Angular was not getting notified of the change in the data.
Edit:
On the 2nd walk through of your code, I see the actual problem. You are reassigning the complete scope.data in the sort() method which is breaking the pass by reference behavior of Javascript (or in any OOPS programming).
The pass by reference will only work if you continue to modify your SAME reference variable. Noticed the word, SAME?? By writing scope.data = lodash.sortBy(scope.data, function (o) {}) you removed the reference of the actual data passed to the directive. Hence the values are not updated.
So to fix this problem, you have a few options:
Change your sorting code to not reassign the complete scope.data variable RECOMMENDED (use inbuilt sort method)
Pass the modified data to the parent scope using scope.$emit()
Or use the $parent property which you don't want to use
The bidirectional binding will update the parent on each digest cycle but the click handler needs to invoke that digest cycle with $apply:
link: function (scope, element, attributes) {
var sorted = undefined;
var col = attributes['sortable'];
var oldClass = 'sorting'
element.addClass(oldClass);
//REPLACE this
//$(element).on("click", sort);
//
//WITH this
element.on("click", function (e) {
sort();
scope.$apply();
});
function changeClass(){
if(sorted=='asc'){
element.removeClass(oldClass);
element.addClass('sorting_asc');
oldClass = 'sorting_asc';
}
else if(sorted=='desc'){
element.removeClass(oldClass);
element.addClass('sorting_desc');
oldClass='sorting_desc';
}
}
The ng-click directive automatically invokes $apply but when the click event is handled by AngularJS jqLite, the code needs to notify the AngularJS framework.
From the Docs:
$apply([exp]);
$apply() is used to execute an expression in angular from outside of the angular framework. (For example from browser DOM events, setTimeout, XHR or third party libraries). Because we are calling into the angular framework we need to perform proper scope life cycle of exception handling, executing watches.
-- AngularJS $rootScope.scope API Reference -- $apply

How to specify execution order in (asynchronous) Angular/Javascript

I have a custom directive (this is just a simplified example). When the directive is clicked, a variable is updated and a function is executed. Both the variable and the function is from the parent scope, accessed via two-way binding and method/behavior binding respectively.
In some cases the parent scope might overrule the directive and force the variable to be a different value than the one set by the directive.
My problem is that the function is called before the variable is updated, probably because the asynchronous behaviour. That means that the variable gets overwritten by the directive.
Is there some way to force the function to be called after the variable has been updated?
This is from the directive's link function:
element.bind('click', function(){
scope.val = 'someValue'; // This should alway be executed first
scope.directiveClicked(); // This should always be executed last
});
The variable and method are accessed like this:
scope: {
val: '=value',
directiveClicked: '&onDirectiveClick'
}
Turned out that it was not called in the wrong order after all, but somehow the updated value was not displayed/stored. Adding a scope.$apply() before notifying the parent function did the trick.
element.bind('click', function(){
scope.val = 'someValue';
scope.$apply();
scope.directiveClicked();
});

$scope or scope in $scope.$watch?

Found such an idea in article:
Notice how the value function takes the scope as parameter (without
the $ in the name). Via this parameter the value function can access
the $scope and its variables.
$scope.$watch( function( scope ) {
return scope.val;
...
instead of what i used to:
$scope.$watch( function() {
return $scope.val;
...
Is it really better? And what is the reasoning behind this way?
From AngularJs docs
function(scope): called with current scope as a parameter.
So it does not change the behavior of your code. However this version prevents a capture of the $scope variable inside the callback :
$scope.$watch(function(scope) {
return scope.val;
}, function(value){ });

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