I have a directive that watches the height of an element. That element is updated by a controller that uses a factory method to get data. Unless I have a $timeout in that factory my watch never gets updated. Curious! Can anyone shed light onto why?
My controller:
$scope.update = function () {
apiService.getLinks(function (response) {
$scope.links = response;
// If I try $scope.$apply() here it says its already in progress, as you'd expect
});
}
quickLinksServices.factory('quickLinksAPIService', function ($http, $timeout) {
quickLinksAPI.getQuickLinks = function (success) {
//without this the watch in the directive doesnt get triggered
$timeout(function () { }, 100);
$http({
method: 'JSON',
url: '/devices/getquicklinkcounts'
}).success(function (response) {
quickLinksAPI.quicklinks = response;
quickLinksAPI.saveQuickLinks();
success(response);
});
}
The directive I'm using is here
Basically angularjs provides $timeout service to trigger function call after specified time, but as I know the use of $timeout is not strictly needed, basically people have habit of writing this I mean they need to trigger watch after specified interval. but in many cases $apply does the trick. The thing you need is $apply(). for your reference please check this
Also many times what happens is angulars $watch executes very faster than you expected so it may send incomplete updates or response In such cases $timeout plays the important role by delaying $watch. You can clear $timeout if $watch is fast enough to your need, which means $timeout is the way to explicitly trigger the $watch, where as $apply can do it by itself. So the use of $timeout or $apply is depends on how your requirement is. Hope this clears you. Good luck.
This is because you are not working with the $http promise in the right way.
Following code is not tested, but shows how you could solve your problem. Note the return $http. Here you return the promise and handle the promise in your code.
$scope.update = function () {
quickLinksServices.quickLinksAPI.getQuickLinks().then(function(data){
$scope.links = data;
}
);
};
quickLinksServices.factory('quickLinksAPIService', function ($http, $timeout) {
quickLinksAPI.getQuickLinks = function (success) { return $http({
method: 'JSON',
url: '/devices/getquicklinkcounts'
});
});
}
If you can't make it work this way because of design decisions, you still have to work with promises ($q). If you want to know more about promises here is a good thread on stackoverflow.
Related
I have seen this issue a lot on SO, but none of the solutions I have tried are working for me. I tried using $apply() which gives me an error stating that the digest cycle is already running. I tried using the Dot "." notation, but nothing changes. I even tried using timeout and promises, but still it does not update in the view.
Here is the HTML:
<th colspan="4" ng-class="{'ssqtrue': deficiencies === false , 'ssqfalse': deficiencies === true}">
<span ng-model="definfo">{{definfo}}</span>
</th>
Here is my Controller code:
$scope.recalculateDashboard = function (goToDashboard) {
contractorService
.calculateScores()
.success(function () {
getscoringDetails();
getDefInfo();
if (goToDashboard) {
$scope.tabs[0].active = true;
}
}).error(function (reason) {
console && console.log(reason.statusText);
genericErrorAlertHandler();
});
};
function getDefInfo() {
contractorService.getDeficiencyInfo()
.success(function (data) {
$scope.$apply(function() {
$scope.definfo = data;
});
if ($scope.definfo == 'No Deficiencies Found') {
$scope.deficiencies = false;
} else {
$scope.deficiencies = true;
}
}).error(function (reason) {
console && console.log(reason.statusText);
genericErrorAlertHandler();
});
}
For the life of me, I can't figure out what is going on here. Any assistance is greatly appreciated!
Regards to <span ng-model="definfo">{{definfo}}</span>
You don't need ng-model directive here. Enough to use {{definfo}} or better way like folks pointed to use ng-bind like:
<span ng-bind="definfo"></span>
I tried using $apply() which gives me an error
Generally, developers use $scope.$apply when get callback from 3d party code like jQuery that stay out of digest cycle. So don't use it. BTW, safety way is to use $timeout for example a.e. wrap as:
$timeout(function () {
$scope.definfo = data;
});
I believe if you don't use Promise chain you can use success and error callbacks (what you actually did) instead to use then()
About $http.get('/SSQV4/SSQV5/Contractor/GetDeficiencyInfo');
$http.get returns original Promise but it also returns status 'Ok', config, headers ... and i'm sure you don't want parse full response in your controller.
So I would create new Promise in your service and fetch results only.
So instead: return $http.get(URL); I would write in Service:
this.getDeficiencyInfo = function () {
var deferred = $q.defer();
$http({method: 'GET', url: URL}).then(function(res){
deferred.resolve(res.data);
}, function (error) {
console.error(error);
deferred.resolve({error:error}); //actually you can reject
});
return deferred.promise;
};
DEMO in fiddle
I figured this out, although I don't know "why" it caused this particular issue. When the page first loads $scope.definfo is set by a variable from another http call. The variable var pagedata = [] holds a lot of information that comes back from the service when the page is first rendered. The scope variable is then set with this code: $scope.definfo = pagedata.DeficiencyInfo. Removing that line and calling the function getDefInfo directly, sets and updates the scope variable as desired. Perhaps one of you gurus could explain why that is to me and others so that it may help others. Thank you to everyone for your help.
I have three elements to this program. First, in the service, I have:
$scope.loaded = MySvc.loaded;
$scope.loaded = false;
Then, in the controller which imports this service, I have a series of introductory things. I need to trigger events in the directives once the controller is done with its asycnhronous work, so in the controller, when that trigger is ready, I do
MySvc.loaded = true;
And then in the directive, which also imports the service, I have,
$scope.loaded = MySvc.loaded;
$scope.$watch('loaded', function (newValue, oldValue) {
The directive triggers when loaded is initialized to 'false', but when I change the value to 'true', nothing happens. The watch simply does not trigger. Why not? How do I fix this?
This is a fine way to do things, and I view it as orthogonal to promises (which are indeed extremely useful). Your problem comes from breaking the linkage you've created with the assignment. Instead try:
$scope.data = MySvc.data;
And append to that, e.g. MySvc.data.loaded = false. This way the data variable is never reassigned, so your linkage between controller and service stays intact.
Then you can either watch data.loaded or watch data as a collection by passing true as the 3rd option to $watch.
As I said in comment, you may have problems with yours scopes overridding the loaded property.
Try using the angular events to solve your problem.
angular.module('test', [])
.controller 'MyController', ($scope, $timeout) ->
$timeout ->
$scope.$broadcast('READY')
, 2000
.directive 'myDirective', ->
scope: {}
template: '<span>{{ value }}</span>'
link: ($scope) ->
$scope.value = "I'm waiting to be ready..."
$scope.$on 'READY', ->
$scope.value = "I'm ready!!"
See this in action (CoffeeScript as it's faster for prototyping but should be clear enough).
You're making this complicated. Use promises instead!!! They're sweet. Promises keep state of their resolution.
.factory('hello_world', function ($q, $timeout) {
var promise = $q.defer;
$timeout(function () {
promise.resolve('hello world');
}, 1000)
return promise.promise;
})
.controller('ctrl', function ($scope, hello_world) {
hello_world.then(function (response) {
console.log("I will only be ran when the async event resolves!");
console.log(response);
});
});
As you can see promises are much better, no watches. No weird gates. Simpler, and sexy. ;)
I get the basic idea of the infinite digest loop and how it happens, but I'm running into the problem. Here is a fiddle demonstrating my code and problem:
http://jsfiddle.net/eS5e5/1/
In the jsfiddle console you'll see the infinite digest loop.
Basically I have to make decisions on data that may not have loaded yet so I need to wait for the promise to resolve using then(). I have a promise called user. There are two different places in the code where I call then() on user.
Just after I define it. I need to set a scope variable based on it.
In another scope method, $scope.isAdmin()
For number 2, it might be asked why I just don't use $scope.user directly in the $scope.isAdmin() method. The problem is, it's possible for $scope.isAdmin() to be called before the async request for the user returns, in which case I need to 'block' before returning from $scope.isAdmin().
My question is, what about $scope.isAdmin() is making angular think that a 'watched' variable has changed and that the digest cycle needs to run again?
$scope.isAdmin() isn't actually changing anything.
Here is the stripped down code:
HTML:
<body ng-controller='myController'>
<div ng-if='isAdmin()'>Hi! <strong>{{ user.username }}</strong> is an Admin!!!</div>
<div ng-if='!isAdmin()'>Hi! <strong>{{ user.username }}</strong> is NOT an Admin!!!</div>
</body>
And the JS:
angular.module('myApp', [])
.factory('myService', function($q, $timeout) {
return {
getUser: function() {
var deferred = $q.defer();
$timeout(function() {
deferred.resolve({ username: 'me', isAdmin: true });
}, 2000);
return deferred.promise;
}
};
})
.controller('myController', function($scope, $q, myService) {
var getUserDeferred = $q.defer();
var user = getUserDeferred.promise;
user.then(function(user) {
$scope.user = user;
return user;
});
$scope.getUser = function() {
return myService.getUser().then(function(user) {
getUserDeferred.resolve(user);
});
};
$scope.isAdmin = function() {
return user.then(function(user) {
return user.isAdmin;
});
};
$scope.getUser();
});
So I finally figured out my own problem and thought I would answer it for others in case someone else may find this info useful.
The crux of the fix had to do with 2 concepts: angular promises and angular watches. By being aware of and applying the 2 concepts together the fix was actually pretty simple.
Everything you put on $scope is 'watched' including functions. Every time something watched changes $scope.$apply() runs again to apply the changes. If a scope function (eg: $scope.isAdmin()) changes its return value from one 'apply' to the next it will trigger another 'apply', until things stabilize and the return value isn't changing.
But in my code I was returning user.then(...) which just returns a new promise (which kept the apply cycle going on forever since the return value kept changing).
In my isAdmin() function I needed to defer its return value until the user actually loaded (any other return value would be meaningless). So I changed the code to check if the user async call had resolved by checking $scope.user and if so returning a valid isAdmin value. If $scope.user was still not defined I would just return the promise I already created.
I changed the $scope.isAdmin() to be:
$scope.isAdmin = function() {
if ($scope.user) {
return $scope.user.isAdmin;
}
return user;
};
This has the same effect as the original code without triggering an infinite apply cycle. Specifically, if the $scope.user has not resolved we still return a promise as before, by returning the user var. Note however that the user var is the same promise not a new one created by then() so the apply cycle stabilizes.
And just for completeness here is the updated jsfiddle:
http://jsfiddle.net/eS5e5/2/
I've implemented a filedrop directive that puts the file dropped in ngModel.
<filedrop data-ng-model="file"></filedrop>
I'm using the following code in my controller:
$scope.$watch('file', function(newVal, oldVal) {
if (newVal) {
var reader = new FileReader();
reader.onload = function(event) {
$scope.parseFile(newVal);
};
reader.readAsDataURL(newVal);
}
}, false);
In $scope.parseFile Im actually parsing the XLSX:
$scope.parseFile = function(file) {
xlsxParser.parse(file).then(function(data) {
console.log("Number of columns", data.datasheet[1].length);
console.log("Number of rows", data.datasheet.length);
$scope.validationErrors = [];
$scope.brand = {
items: []
};
$scope.dataItems = [];
$scope.datasheetValidate(data.datasheet, $scope.brand);
$scope.datasheetData(data.datasheet, $scope.dataItems);
if ($scope.validationErrors.length == 0) $scope.validationErrors.push("Nice work, no validation errors");
//$scope.$apply(function(){});
}, function(err) {
console.log('error', err);
});
}
As you can see, I commented out the //$scope.$apply(function(){}); in the body....
BUT, I need it in order to have my web page updated with the scope changes (e.g. show the validationErrors)
How come I need the $scope.$apply?
It doesn't matter that you're using $scope.$watch. That doesn't help when you're calling $scope.parseFile from inside reader.onload callback, which is outside of Angular realm.
$scope.$apply should be used inside the callback itself:
reader.onload = function(event) {
$scope.$apply(function(){
$scope.parseFile(newVal);
});
};
$scope.$watch and $scope.$apply are complementary.
$scope.$watch registers a new watcher on the scope. Whenever there's a digest cycle, the watch function runs.
$scope.$apply triggers a digest cycle--that is to say, if nothing ever called $scope.$apply, no watchers would ever be run at all.
For built-in directives that deal with user input (ng-click, ng-keydown, etc.) and built-in services ($http, $location, $timeout, etc.), Angular calls $scope.$watch for you. However, any time you deal with asynchronous code that lives outside of these built-in directives or services, you must tell Angular that it should kick off a new digest cycle by calling $apply yourself.
As mentioned by Stewie, you should try to keep your $apply calls as close to the asynchronous operation as possible; in this case, it means using it at the very top level of the onload callback.
The only reason i see this happen if xlsxParser.parse(file).then is not a angular promise callback method.
And as confirmed by #Sander this indeed is not a angular promise, so option here is to keep using the existing $scope.$apply call.
I'm want to create service with $http with angular.js as is shown in this plunk
But when I try to do it in angular 1.2 I can not have more than one function into the factory as is done here
Works in angular 1.0.2
app.factory('myService', function($http, $q) {
return {
getFoo: function() {
var deferred = $q.defer();
$http.get('foo.json').success(function(data) {
deferred.resolve(data);
}).error(function(){
deferred.reject();
});
return deferred.promise;
},
getBar: function(callback) {
$http.get('foo.json').success(callback);
},
testHttpGetResult: function (){
return $http.get('foo.json');
}
}
});
How should I do it in angular 1.2? must I use a different approach(not a factory)?
Thanks in advance.
Edited
It Works, because have only a function(getFoo)
app.factory('myService', function($http, $q) {
return {
getFoo: function() {
var deferred = $q.defer();
$http.get('foo.json').success(function(data) {
deferred.resolve(data);
}).error(function(){
deferred.reject();
});
return deferred.promise;
}
});
As of Angular 1.2, promise unwrapping is optional, and it will be disabled entirely in future versions of Angular.
In other words, your clean version as of 1.2 requires an opt-in and won't work at all in the future.
I would guess that if you look at the code you have defined "getFoo" as follows:
getFoo: function() {
// ... code here ...
return deferred.promise;
},
When you call this routine you use the following code:
// "foo" represents a PROMISE, not the actual json value!
$scope.foo = myService.getFoo();
The routine isn't returning the value you expect though - rather it is returning a promise which is why the scope doesn't have what you expect. You need to use a ".then" syntax to actually get the data result from your routine. For some reason it seems that older versions of angular might have been doing this for you (possibly)?
Anyways, the issue isn't whether or not you can define multiple routines (you can), but how you call those routines.
Edit
In case I didn't make it clear enough the calling method marked "the clean way" shouldn't (and doesn't work). You should use the following instead:
// The clean way - FIXED
myService.getFoo().then(function (data){
$scope.foo = data;
});
Good luck!
I know this is an old post but I found this article that helped me a lot.
Hope this will help others. It fallows drew_w's Answers.
http://blog.brunoscopelliti.com/angularjs-promise-or-dealing-with-asynchronous-requests-in-angularjs
and this one is even better:
show route only after all promises are resolved
http://blog.brunoscopelliti.com/show-route-only-after-all-promises-are-resolved