I have below div element with nghide
<div ng-hide="showdiv" class="btnshowall">
<a class="button button-block round outline"
style="background: transparent !important;" >
Show All
</a>
</div>
and controller as below
.controller('mapCtrl', ['$scope', '$stateParams','User','$cordovaGeolocation','geoFireFac','GoogleMapFac','ConnectivityMonitor','PhysioFac','User',
function ($scope, $stateParams,User,$cordovaGeolocation,geoFireFac,GoogleMapFac,ConnectivityMonitor,PhysioFac,User) {
console.log('called mapctrl');
GoogleMapFac.setUserLoc($scope.map);
$scope.showdiv = User.getShowDiv();
}])
and User service as
.service('User', ['ToastFac',function(ToastFac){
return {
showDiv : false,
changeShowDiv : function(){
console.log('in changeShowDiv before change '+this.showDiv);
this.showDiv = !this.showDiv;
console.log('in changeShowDiv after change '+this.showDiv);
},
getShowDiv : function(){
return this.showDiv;
}
I am invoking User.changeShowDiv() from google map's marker click event like below
google.maps.event.addListener(marker, 'click', function () {
alert('store id '+marker.get('store_id'));
if(User.showDiv){
console.log('in if');
User.changeShowDiv();
console.log('User.showDiv '+User.showDiv);
}
else{
console.log('in else');
User.changeShowDiv();
console.log('User.showDiv '+User.showDiv);
}
});
logs are coming as expected
in else
services.js:123 in changeShowDiv before change false
services.js:125 in changeShowDiv after change true
services.js:218 User.showDiv true
services.js:211 in if
services.js:123 in changeShowDiv before change true
services.js:125 in changeShowDiv after change false
services.js:213 User.showDiv false
services.js:216 in else
services.js:123 in changeShowDiv before change false
services.js:125 in changeShowDiv after change true
services.js:218 User.showDiv true
By default, as User.showDiv variable is false, showAll button is visible. But button is not hiding & coming by marker click events.
Could someone guide me what I am missing.
Events that come from outside the AngularJS framework need to be brought into the AngularJS framework with $apply:
google.maps.event.addListener(marker, 'click', function () {
alert('store id '+marker.get('store_id'));
if(User.showDiv){
console.log('in if');
User.changeShowDiv();
console.log('User.showDiv '+User.showDiv);
}
else{
console.log('in else');
User.changeShowDiv();
console.log('User.showDiv '+User.showDiv);
}
//IMPORTANT
$scope.$apply();
});
AngularJS modifies the normal JavaScript flow by providing its own event processing loop. This splits the JavaScript into classical and AngularJS execution context. Only operations which are applied in the AngularJS execution context will benefit from AngularJS data-binding, exception handling, property watching, etc... You can also use $apply() to enter the AngularJS execution context from JavaScript. Keep in mind that in most places (controllers, services) $apply has already been called for you by the directive which is handling the event. An explicit call to $apply is needed only when implementing custom event callbacks, or when working with third-party library callbacks.
— AngularJS Developer Guide - Integration with the browser event loop
ALSO
Be sure to fix the ng-hide and the controller:
<div ng-hide="showdiv()" class="btnshowall">
$scope.showdiv = function() {
return User.getShowDiv();
};
In the above code, the ng-hide directive will execute the showdiv() function on each digest cycle and update the visibility of the element accordingly.
You're retrieving value from User.getShowDiv method once only. But when it gets change your are not updating showdiv scope variable. To update value each time you can directly bind the reference of User.getShowDiv method to showdiv scope variable like below
$scope.showdiv = User.getShowDiv;
There after call showdiv method on HTML, which will eventually evaluate value on each digest cycle unlike other bindings.
ng-hide="showdiv()"
Even above would not solve your problem. Basically you're updating some variable from outside context Angular which is click event. So you have to run digest cycle manually right after updating value from click event listener ran. Just do use $timeout(angular.noop) to fire digest cycle safely.
google.maps.event.addListener(marker, 'click', function () {
alert('store id '+marker.get('store_id'));
if(User.showDiv){
//Code here
}
else{
//Code here
}
//manually triggering digest loop to make binding in sync
$timeout(angular.noop); //It will run new digest cycle.
});
Related
Here is the problem
<div id="my-id" ng-click="moveOn(10)">Click me</div>
In the directive I have
scope.moveOn = function (val) {
$location.search('id', val);
}
And finally in the parent of this directive I listen for a change of this id
$scope.$watch(function () {
return $routeParams.id;
}, function (newId, oldId) {
...
});
This setup works great, after the $location.search is called the $watcher is triggered immediately. But now I also have a directive which does it slightly different, as follows:
element.find('#my-id').click(function (val) {
$location.search('id', val);
});
In the template there is no ng-click!
In this situation I can also see that the call to $location.search is made, but now it takes a very long time (a couple of seconds) before the watcher goes off.
So for some reason there must be a difference between ngClick and binding to a click event. Any suggestions what might be going on here ?
You are updating angular within an event that is outside of angular.
Try using $apply to notify angular of the change so it can run a digest
element.find('#my-id').click(function (val) {
scope.$apply(function(){
$location.search('id', val);
});
});
I would like to fire all $watch/$observe listeners even if watched/observed value didn't change. This way I could provide a "testing only" feature to refresh current page/view without user interaction. I've tried to call $apply/$digest but that didn't worked:
$timeout(function(){
$scope.$apply();
});
$timeout(function(){
$scope.$digest();
});
Is there any other way to do it?
Best Regards,
Executing $scope.$apply() will trigger digest cycle as it internally calls $digest, below is example of manual change.
number variable won't get bound as timeout brings it out of angulars scope.
setTimeout(function () {
$scope.number = Math.random();
});
however you can "force" it to show up by manually applying scope changes:
setInterval(function () {
$scope.$apply();
}, 100);
Demos:
No change / Change with manual updates
This will not trigger watchers though. From $digest implementation, it checks if value has changed since the last watch evaluation and will run callback only if it did.
if ((value = watch.get(current)) !== (last = watch.last) ... [rootScope.js]
Therefore you will need somehow change value of the last execution and it's possible to do via $$watchers object on the scope:
$scope.digest = function () {
setTimeout(function () {
angular.forEach($scope.$$watchers, function (w) {
w.last.value = Math.random();
});
$scope.$apply();
});
}
DEMO
How about to use the $emit function then capture that event with $on function?
Within an $emit() event function call, the event bubbles up from the child scope to the parent scope. All of the scopes above the scope that fires the event will receive notification about the event.
We use $emit() when we want to communicate changes of state from within our app to the rest of the application.
_.bind($scope.$emit, $scope, 'myCustomEvent');
then on the capture phase:
$scope.$on('myCustomEvent', function() {
// do something
});
edit I've now fixed the bug by using ng-click in the directive's template, but I'm still interested to know what ng-click does that's different to element.bind and why it's important
I did have a ng-select box to update my model
<td><select
ng-model="tune.dummyStandard"
ng-options="opt.value as opt.label for opt in dropdowns.playback"
ng-change="update()" >
<option value="">-- rate performance --</option>
</select></td>
At the end of a long validation process, it created a new $http request (via $resource.$update) and saved the record.
I've now changed to use a custom directive
<td j-performance-rater data-tune="tune"></td>
which attaches the following listener (the actual listener is a little more complicated but I've checked quite rigorously that it always fires tune.resource.$update as expected)
element.bind('click', function (ev) {
// code for validation and setting of model props
tune.resource.$update();
});
Now odd-numbered clicks create the $http object but don't send, and the even-numbered clicks send the previously created $http object and create and successfully send a new one. This happens consistently even when the clicks are on different instances of the directive. I've tried playing around with $digest and $apply on the scope but they don't have any impact (and I'm not sure if they should as $http seems to me like it should behave independently of the digest cycle).
Can anyone think what the problem might be?
Full directive code
directives.directive('jPerformanceRater', function () {
return {
// transclude: true,
scope: true,
templateUrl: '/views/directives/performance-rater.html',
compile: function(element, attrs) {
return function (scope, element, attrs) {
var tune = scope[attrs.tune || 'tune'];
scope.tune = tune.tune;
element.bind('click', function (ev) {
ev.cancelBubble = true;
var btn = ev.target.className.indexOf('icon') > -1 ? ev.target.parentNode : ev.target;
if (btn.className.indexOf('btn') === -1) {
return;
}
tune.dummyStandard = +btn.textContent;
tune.update(); // most of the validation happens in here and then it calls resource.$update()
});
element.addClass('performance-rater');
};
}
};
});
And the template
<span class="btn btn-small" ng-repeat="rating in dropdowns.playback" type="checkbox"
title="{{rating.label}}">{{rating.value}}<i class="icon icon-star{{rating.value == 0 ? '-empty' : ''}}"></i></span>
Changing :
element.bind('click', function (ev) {
tune.resource.$update();
});
to:
element.bind('click', function (ev) {
scope.$apply(function(){
tune.resource.$update();
});
});
should do the trick.
Diference between bind('click') and ng-click is that ng-click triggers angular $digest cycle whereas bind('click') does not.
Any $http or $resource requests trigger request to server only when invoked within angular $digest cycle (internal angular event loop cycle). So in your case you invoked resource.update but outside angular $digest cycle, so request to server wasn't made but only 'scheduled' until next time angular event loop was triggered (for example when ng-change was executed) and request to server was initiated.
I have a function in an angular service containing an ajax call doing the following:
function performCall($scope){
$scope.dataFail = false;
var promise = doAjaxCall();
promise.then(function(data){
if(data.rootElement){
//process...
}else{
$scope.dataFail=true;
}
}
}
And a $watch expression in my controller watching dataFail and displaying a dialog with an option to call performCall when the dialog is confirmed:
$scope.$watch('dataFail', function(dataFail){
if(dataFail){
//open dialog
$( "#ajaxFailurePopup" ).dialog({
zIndex: 3003,
title: "Note:",
modal:true, // Disable controls on parent page
buttons: {
Ok: {
text: 'Retry >',
"class" : 'ppButton floatRight',
click:
function() {
service.performCall($scope);
$("#ajaxFailurePopup").remove();
}
}
}
});
};
});
This works fine on initialisation when the ajax call first fails. However, after this no changes made to dataFail are registered by the $watch. Does anybody have any suggestions?
Resolved by wrapping call to performCall in $scope.apply:
$scope.apply(
service.performCall($scope);
)
The dataFail flag was being set in the performCall method. Apologies.
There are couple of problems with your code:
In Angular, $watch expression callback function is called only if expression has changed the value. Since you're never resetting your 'dataFail' flag the expression never gets called in subsequent calls. You should set your flag variable to false inside the $watch expression.
In dialog you are calling $("#ajaxFailurePopup").remove(); which removes the #ajaxFailurePopup element from the DOM, hence the dialog is unable to initialize again. You should use $('#ajaxFailurePopup').dialog('destroy');
Working plnkr: http://embed.plnkr.co/wcooiJ
Since you say that the watch fires on the first failed request, but no on subsequent failed requests, is it possible that you're not resetting $scope.dataFail to false for successful requests? If $scope.dataFail is only set to false during initialization, the value never changes and your watch won't get called. Setting $scope.dataFail to true if it´s already true won't fire the watch since Angular can't tell that it changed.
AngularJS noob here, on my path to the Angular Enlightenment :)
Here's the situation:
I have implemented a service 'AudioPlayer' inside my module 'app' and registered like so:
app.service('AudioPlayer', function($rootScope) {
// ...
this.next = function () {
// loads the next track in the playlist
this.loadTrack(playlist[++playIndex]);
};
this.loadTrack = function(track) {
// ... loads the track and plays it
// broadcast 'trackLoaded' event when done
$rootScope.$broadcast('trackLoaded', track);
};
}
and here's the 'receiver' controller (mostly for UI / presentation logic)
app.controller('PlayerCtrl', function PlayerCtrl($scope, AudioPlayer) {
// AudioPlayer broadcasts the event when the track is loaded
$scope.$on('trackLoaded', function(event, track) {
// assign the loaded track as the 'current'
$scope.current = track;
});
$scope.next = function() {
AudioPlayer.next();
};
}
in my views I show the current track info like so:
<div ng-controller="PlayerCtrl">
<button ng-click="next()"></button>
// ...
<p id="info">{{current.title}} by {{current.author}}</p>
</div>
the next() method is defined in the PlayerCtrl, and it simply invokes the same method on the AudioPlayer service.
The problem
This works fine when there is a manual interaction (ie when I click on the next() button) - the flow is the following:
PlayerCtrl intercepts the click and fires its own next() method
which in turn fires the AudioPlayer.next() method
which seeks the next track in the playlist and calls the loadTrack() method
loadTrack() $broadcasts the 'trackLoaded' event (sending out the track itself with it)
the PlayerCtrl listens the broadcast event and assigns the track to the current object
the view updates correctly, showing the current.title and current.author info
However, when the next() method is called from within the AudioService in the 'background' (ie, when the track is over), all the steps from 1 to 5 do happen, but the view doesn't get notified of the change in the PlayerCtrl's 'current' object.
I can see clearly the new track object being assigned in the PlayerCtrl, but it's as if the view doesn't get notified of the change. I'm a noob, and I'm not sure if this is of any help, but what I've tried is adding a $watch expression in the PlayerCtrl
$scope.$watch('current', function(newVal, oldVal) {
console.log('Current changed');
})
which gets printed out only during the 'manual' interactions...
Again, like I said, if I add a console.log(current) in the $on listener like so:
$scope.$on('trackLoaded', function(event, track) {
$scope.current = track;
console.log($scope.current);
});
this gets printed correctly at all times.
What am I doing wrong?
(ps I'm using AudioJS for the HTML5 audio player but I don't think this is the one to blame here...)
When you have a click event the $scope is updated, without the event you'll need to use $apply
$scope.$apply(function () {
$scope.current = track;
});
As it's not safe to peek into the the digest internals, the easiest way is to use $timeout:
$timeout(function () {
$scope.current = track;
}, 0);
The callback is executed always in the good environment.
EDIT: In fact, the function that should be wrapped in the apply phase is
this.loadTrack = function(track) {
// ... loads the track and plays it
// broadcast 'trackLoaded' event when done
$timeout(function() { $rootScope.$broadcast('trackLoaded', track); });
};
Otherwise the broadcast will get missed.
~~~~~~
Actually, an alternative might be better (at least from a semantic point of view) and it will work equally inside or outside a digest cycle:
$scope.$evalAsync(function (scope) {
scope.current = track;
});
Advantage with respect to $scope.$apply: you don't have to know whether you are in a digest cycle.
Advantage with respect to $timeout: you are not really wanting a timeout, and you get the simpler syntax without the extra 0 parameter.
// apply changes
$scope.current = track;
try {
if (!$scope.$$phase) {
$scope.$apply($scope.current);
}
} catch (err) {
console.log(err);
}
Tried everything, it worked for me with $rootScope.$applyAsync(function() {});