AngularJS - triggering $watch/$observe listeners - javascript

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
});

Related

ng-hide is not getting updated dynamically

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.
});

AngularJs stop $interval for specific controller

I was used $interval to run certain function in first controller. After redirect to second controller, still $interval registered function (implemented in first controller) was running. I want to stop $interval services without using destroy function.
Note: Intervals created by this service must be explicitly destroyed
when you are finished with them. In particular they are not
automatically destroyed when a controller's scope or a directive's
element are destroyed. You should take this into consideration and
make sure to always cancel the interval at the appropriate moment. See
the example below for more details on how and when to do this.
You could use the cancel method to clean $interval object. You should cancel out the interval while leaving the controller.
For that you need to add eventListener over $scope's $destroy event, you could do cleaning stuff (eg. canceling interval object, canceling timeout object).
Code
var myInterval = $interval(function(){
//your interval code
}, 1000)
//register this listener inside your controller where the interval belongs.
$scope.$on('$destroy', function(){
$interval.cancel(myInterval)
});
Sample Plunkr
Add the interval variable in rootScope. And cancel it in the second controller whenever it gets opened as shown below.
First Controller
$rootScope.stopInterval = $interval(function(){
//code goes here
}, 1000);
Second Controller
$interval.cancel($rootScope.stopInterval);
Note: Only flaw in this case is you need to stop it in all the controllers you use after first controller, but a reliable one.

angular weirdness with element.click and $location.search

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);
});
});

In AngularJS, how do I add a $watch on the URL hash?

Using Angular, how can I add a watch that is fired when the hash is updated, either by my application, or when the browser updates the URL from either the URL bar or the back/forward buttons?
$scope.$watch accepts function as the first argument, so you can do this:
$scope.$watch(function () {
return location.hash
}, function (value) {
// do stuff
});
But I would recommend using events, such as $routeChangeSuccess for default router:
$scope.$on("$routeChangeSuccess", function () {})
or $stateChangeSuccess for ui-router
$scope.$on("$stateChangeSuccess", function () {})
$locationChangeSuccess could be better.
$scope.$on('$locationChangeSuccess', function(event, newUrl, oldUrl){
// TODO What you want on the event.
});
This works too if you're okay with not using the angular $watch.
Basically, you watch 'hashchange' windows event. whatever angularJS does is a wrapper around this. For example,
$($window).bind('hashchange', function () {
// Do what you need to do here like... getting imageId from #
var currentImageId = $location.search().imageId;
});
location.hash can be updated internally by angular or externally by the user or the browser (click link in bookmarks). If it is updated internally angular runs a $scope.$apply(). If an external event updates location.hash $watch does only fire UNTIL AFTER the next $scope.$apply(), e.g. a user pushes a button within your app.
If you wish to use a $watch, add an additional event listener to "hashchange" to call $apply, or add all functionality to the native DOM listener AND do not forget to call $apply(), as this is an external call.
window.addEventListener("hashchange", function(){ $scope.$apply(); }, false);
OR ...
window.addEventListener("hashchange", function(){ me._locationHashChanged(); $scope.$apply(); }, false);

AngularJS : service $broadcast and $watch not triggering on receiving controller

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() {});

Categories

Resources