I have an ng-repeat element that is only populated the second time I press a button. The button makes a GET call to an API, a JSON response is received and a $scope.movies is set equal to the JSON.
$scope.movies = [];
$scope.searchMovie = function()
var params = {
query: 'monsters',
include_adult: true
}
tmdb.call("/search/movie", params,
function(e){
$scope.movies = e;
},
function(e){
console.log("Error" + e)
});
$scope.searchMovie is connected to a button
The reason why it doesn't update, is that Angular works with something called Digest cycles. If you are giving a callback to a 3rd party library it won't trigger an angular digest, in which case you need to trigger one yourself. I suggest reading more info here:
https://docs.angularjs.org/api/ng/type/$rootScope.Scope
The basic ways to trigger a digest are through $scope.$apply , $scope.applyAsync, and $timeout. I suggest you read up on these concepts because they will be very important down the road.
Related
So I've got the following code...
testApp.controller(...) {
$scope.results = [];
$scope.hasData = true;
$scope.results.push({
"name": "test"
}); // WORKS
db.get('table_people').then(function(response) {
console.log('success');
$scope.results.push({
"name": "test"
});
}); // this DOESN'T WORK even though the "success" message is printed...
});
And as you can tell from the comments, the first push to the array works, but the latter one doesn't. Top one can be printed out in the Angular template using {{ results }} but the latter one returns an empty array.
Edit: A solution has been found by using $timeout as the digest cycle wasn't running but sort of feels like a hacked together solution.
Edit: Solution...
db.get('table_people').then(function (response) {
console.log('success');
$timeout(function () {
$scope.results = response.data;
});
});
The solution code is slightly different as I no longer need the test data anymore due to the code functioning and can apply the response data directly.
You're missing a $digest cycle tick. Doing $scope.$digest() after you've pushed the data into $scope.results should fix the issue. Using $timeout is a bit of an overkill in this situation (and additional service to inject).
Hi im currenty using $route.reload to refresh the content of my controller Every time I update my Database. the problem is when updating huge list of data, Every Time I update my Database and run $route.reload my browser lose its ability to scroll up or down my browser, it works fine with smaller list of Data.
below is a sample of my code
$scope.Undone = function(id){
$scope.index = $scope.GetID ;
CRUD.put('/UndoJda/'+$scope.index).then(function(response){
toastr.info('Jda has been activated.', 'Information');
$route.reload();
});
}
Your best bet would be some sort of lazy loading/pagination. So in case it's a really large list, like in the tenths of thousands, it might even be a DOM rendering problem. Also, if that isn't the case, you should try using AngularJS's bind once(Available since 1.3), as well as track by which does not create a watcher for each object on the scope, in your template. Assuming you are using ngRepeat, let's say something like this:
...<ul>
<li ng-repeat="item in Items">
<b>{{item.name}}</b>
</li>
</ul>
Change that to something like the following, in case the data does not update often:
...<ul>
<li ng-repeat="item in Items track by $index">
<b>{{::item.name}}</b>
</li>
</ul>
As a side note, try to always have a dot in your model's name. $scope.Something.list, for eaxample. ("If you don't have a dot, you are doing it wrong" - Misko Hevery himself said this.).
When the data is huge, try to use $timeout and reload the page.
This would prevent very fast refreshes and will keep your page responsive.
$scope.Undone = function(id){
$scope.index = $scope.GetID ;
CRUD.put('/UndoJda/'+$scope.index).then(function(response){
toastr.info('Jda has been activated.', 'Information');
$timeout(function() {
$route.reload();
}, 200);
});
}
You can do it by using $interval
$interval(function() {
CRUD.put('/UndoJda/'+$scope.index).then(function(response){
toastr.info('Jda has been activated.', 'Information');
// Update scope variable
});
}, 2000);
and also don't use $route.reload();. because Angularjs supporting SPA (Single Page Application). if you using $route.reload();. Every time page will loading, So it's not good. you need just call the Service code in inside of interval.
First I would recommend removing usage of $route.reload(), your use case doesn't require the view re-instantiating the controller. Instead you should update the $scope variable that holds the collection of entities your presenting in the view. You will also want to consider adding UX features such as a loading indicator to inform the user about the long running task.
Something similar too the code below would achieve what your looking for. I am unaware of what your CRUD js object instance is, but as long as its Angular aware you will not need to use $timeout. Angular aware usually means non third party APIs, but you can use $q to assist in exposing third party ajax results to angular.
// angular module.controller()
function Controller($scope, EntityService) {
$scope.entityCollection = [];
$scope.loadingData = false; // used for loading indicator
// Something will initialize the entity collection
// this is however your already getting the entity collection
function initController() {
$scope.refreshCollection();
}
initController();
$scope.refreshCollection = function() {
$scope.loadingData = true;
EntityService.getEntitites().then(function(resp) {
$scope.entityCollection = resp;
$scope.loadingData = false;
});
}
$scope.Undone = function(id) {
$scope.index = $scope.GetID ;
CRUD.put('/UndoJda/' + $scope.index).then(function(response){
toastr.info('Jda has been activated.', 'Information');
$scope.refreshCollection();
});
}
}
// angular module.factory()
function EntityService($q, $http) {
return {
getEntitites: function() {
var deferred = $q.defer();
$http.post('/some/service/endpoint').then(function(resp) {
deferred.resolve(resp);
});
return deferred.promise;
}
}
}
I'm currently polling the server to check for new data, and then update the model in an AngularJS app accordingly. He're roughly what I'm doing:
setInterval(function () {
$http.get('data.json').then(function (result) {
if (result.data.length > 0) {
// if data, update model here
} else {
// nothing has changed, but AngularJS will still start the digest cycle
}
});
}, 5000);
This works fine, but most of the requests will not result in any new data or data changes, but the $http service doesn't really know/care and will still trigger a digest cycle. I feel this is unnecessary (since the digest cycle is one of the heaviest operations in the app). Is there any way to still be able to use $http but somehow skip the digest if nothing has changed?
One solution would be to not use $http but jQuery instead, and then call $apply to let Angular know that the model has changed:
setInterval(function () {
$.get('data.json', function (dataList) {
if (dataList.length > 0) {
// if data, update model
$scope.value = dataList[0].value + ' ' + new Date();
// notify angular manually that the model has changed.
$rootScope.$apply();
}
});
}, 5000);
While this seems to work, I'm not sure it's a good idea. I would still like to use pure Angular if possible.
Anyone got any suggestions for improvements to the approach above or a more elegant solution entirely?
P.S. The reason I'm using setInterval instead of $timeout is because $timeout would also trigger a digest cycle which would be unnecessary in this case and only add to the "problem".
Solution provided by AngularJS #doc
AngularJS recommends to use a PERF trick that would bundle up a few $http responses in one $digest via $httpProvider. This again, is not fixing the problem, it's just a sedative :)
$httpProvider.useApplyAsync(true)
Saving the $$watchers solution - risky and not scalable
Firstly, the accepted solution is not scalable - there's no way you're going to do that $watchers trick on a 100K lines of JS code project - it's out of the question.
Secondly, even if the project is small, it's quite risky! What happens for instance if another ajax call arrives that actually needs those watchers?
Another (feasible) risky solution
The only alternative to achieve this without modifying AngularJS code would be to set the $rootScope.$$phase to true or '$digest', make the $http call, and set back the $rootScope.$$phase to null.
$rootScope.$$phase = true;
$http({...})
.then(successcb, failurecb)
.finally(function () {
$rootScope.$$phase = null;
});
Risks:
1) other ajax calls might try to do the same thing --> they need to be synchronized via a wrapping ajax service (over $http)
2) user can trigger UI actions in between, actions that will change the $$phase to null and when the ajax call will come back, and still trigger the $digest
The solution popped after scanning AngularJS source code - here's the line that saves the situation: https://github.com/angular/angular.js/blob/e5e0884eaf2a37e4588d917e008e45f5b3ed4479/src/ng/http.js#L1272
The ideal solution
Because this is a problem that everyone is facing with AngularJS, I think it needs to be addressed systematically. The answers above are not fixing the problem, are only trying to avoid it.
So we should create a AngularJS pull request that would allow us to specify via $httpProvider a config that would not trigger a digest for a specific $http request. Hopefully they agree that this needs to be addressed somehow.
Web sockets would seem to be the most elegant solution here. That way you don't need to poll the server. The server can tell your app when data or anything has changed.
You can do it by this trick :
var watchers;
scope.$on('suspend', function () {
watchers = scope.$$watchers;
scope.$$watchers = [];
});
scope.$on('resume', function () {
scope.$$watchers = watchers;
watchers = null;
});
With this you will remove your scope or reinsert it on the $digest cycle.
You have to manage events to do that of course.
Refer to this post :
Remove and restore Scope from digest cycles
Hope it helps !
I thought 2 ways binding was angular thing:
I can post from a form to the controller, and if I refresh the page I see my input on the page:
$scope.loadInput = function () {
$scope.me.getList('api') //Restangular - this part works
.then(function(userinput) {
$scope.Input = $scope.Input.concat(userinput);
// scope.input is being referenced by ng-repeater in the page
// so after here a refresh should be triggered.
},function(response){
/*#alon TODO: better error handling than console.log..*/
console.log('Error with response:', response.status);
});
};
In the html page the ng-repeater iterates over the array of for i in input. but the new input from the form isn't shown unless I refresh the page.
I'll be glad for help with this - thanks!
Try $scope.$apply:
$scope.loadInput = function () {
$scope.me.getList('api') //Restangular - this part works
.then(function(userinput) {
$scope.$apply(function(){ //let angular know the changes
$scope.Input = $scope.Input.concat(userinput);
});
},function(response){
/*#alon TODO: better error handling than console.log..*/
console.log('Error with response:', response.status);
});
};
The reason why: Your ajax is async, it will execute in the next turn, but at this time, you already leaves angular cycle. Angular is not aware of the changes, we have to use $scope.$apply here to enter angular cycle. This case is a little bit different from using services from angular like $http, when you use $http service and handle your response inside .success, angular is aware of the changes.
DEMO that does not work. You would notice that the first click does not refresh the view.
setTimeout(function(){
$scope.checkboxes = $scope.checkboxes.concat([{"text": "text10", checked:true}]);
},1);
DEMO that works by using $scope.$apply
setTimeout(function(){
$scope.$apply(function(){
$scope.checkboxes = $scope.checkboxes.concat([{"text": "text10", checked:true}]);
});
},1);
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() {});