All:
I am pretty new to angular digest, right now, when I use Promise, in its then function, I have to use $scope.$digest() to make scope variable change takes effect on other place, like:
Here I use a Promise to emulate $http request, my confuse is in $scope.getdata, why I need to call $scope.$digest(), I thought $scope.disable should be watched by angular automatically.
var app = angular.module("vp", []);
app
.service("srh", function($http){
var busy = false;
this.get = function(url){
if(!busy){
busy = true;
var p = new Promise(function(res, rej){
$timeout(function(){
res("data is fetched");
}, 3000);
})
.then(function(data){
busy = false;
return data;
}, function(data){
busy = false;
return data;
});
return p;
}else {
return null;
}
}
})// end of service
.controller("main", function($scope, srh){
$scope.disable = false;
$scope.getdata = function(){
var dp = srh.get("");
if( dp ) {
$scope.disable = true;
dp.then(function(data){
console.log(data);
$scope.disable = false;
$scope.$digest()
})
}
}
})
Use $q angular promises which will internally handle all digest requirements.
Whenever you use events outside of angular core that modify scope you need to tell angular so it can update view
Related
How does angular watch changes in a model?
app.controller('ProjectsController', function (ProjectsRepository) {
var pj = this;
pj.PageIndex = 1;
pj.PageSize = 50;
var req = ProjectsRepository.Get(pj);
faraAjaxConfig(req)
.success(function (result) {
console.log(result);
if (result.Success) {
pj.Rows = result.Rows; // How detect changes in this and apply to ng-repeat
}
else {
faraAjaxMessage(result.Messages, true);
}
});
});
It looks like you're not using the built in $http, which means you'll need to $scope.$apply() to kick off a digest cycle, which is where angular does its checking. In general, any data retrieved from an async source not using an angular service will need to notify angular of the changes this way. Websockets, ajax, web workers, etc.
(I'd recommend just using $http instead, cleaner all around)
app.controller('ProjectsController', function (ProjectsRepository, $scope) {
var pj = this;
pj.PageIndex = 1;
pj.PageSize = 50;
var req = ProjectsRepository.Get(pj);
faraAjaxConfig(req)
.success(function (result) {
console.log(result);
if (result.Success) {
pj.Rows = result.Rows; // How detect changes in this and apply to ng-repeat
}
else {
faraAjaxMessage(result.Messages, true);
}
$scope.$apply();
});
});
I'm using &http in angular js to make REST calls. And when I make a new call, I want to dismiss the previous call. So I wrap the data call in the service and call the service in the controller. And I'm using the global parameter to save the last call object. So whenever I call the function getsth(), it will replace the lastcall with the new one. But when I debug, it did replace the lastcall with new one, but the previous then still triggers. One solution is the cancel the previous call and I tried it works. But my question is can I overwrite the $http object so that I don't have to handle it. Thanks
Controller:
var lastCall;
$scope.getsth = function(){
lastcall = service.datacall();
lastcall.then()
}
Service:
service.datacall = function(){
var promises = [];
promises.push($http({url:method...}).then(function))
return $q.all(promises);
}
This blogpost explains your use-case pretty well:
http://odetocode.com/blogs/scott/archive/2014/04/24/canceling-http-requests-in-angularjs.aspx
app.factory("movies", function($http, $q){
var getById = function(id){
var canceller = $q.defer();
var cancel = function(reason){
canceller.resolve(reason);
};
var promise =
$http.get("/api/movies/slow/" + id, { timeout: canceller.promise})
.then(function(response){
return response.data;
});
return {
promise: promise,
cancel: cancel
};
};
return {
getById: getById
};
});
app.controller("mainController", function($scope, movies) {
$scope.movies = [];
$scope.requests = [];
$scope.id = 1;
$scope.start = function(){
var request = movies.getById($scope.id++);
$scope.requests.push(request);
request.promise.then(function(movie){
$scope.movies.push(movie);
clearRequest(request);
}, function(reason){
console.log(reason);
});
};
$scope.cancel = function(request){
request.cancel("User cancelled");
clearRequest(request);
};
var clearRequest = function(request){
$scope.requests.splice($scope.requests.indexOf(request), 1);
};
});
I'm using an Angular factory that retrieves data from a feed and does some data manipulation on it.
I'd like to block my app from rendering the first view until this data preparation is done. My understanding is that I need to use promises for this, and then in a controller use .then to call functions that can be run as soon as the promise resolves.
From looking at examples I'm finding it very difficult to implement a promise in my factory. Specifically I'm not sure where to put the defers and resolves. Could anyone weigh in on what would be the best way to implement one?
Here is my working factory without promise:
angular.module('MyApp.DataHandler', []) // So Modular, much name
.factory('DataHandler', function ($rootScope, $state, StorageHandler) {
var obj = {
InitData : function() {
StorageHandler.defaultConfig = {clientName:'test_feed'};
StorageHandler.prepData = function(data) {
var i = 0;
var maps = StorageHandler.dataMap;
i = data.line_up.length;
while(i--) {
// Do loads of string manipulations here
}
return data;
}
// Check for localdata
if(typeof StorageHandler.handle('localdata.favorites') == 'undefined') {
StorageHandler.handle('localdata.favorites',[]);
}
},
};
return obj;
});
Here's what I tried from looking at examples:
angular.module('MyApp.DataHandler', []) // So Modular, much name
.factory('DataHandler', function ($rootScope, $q, $state, StorageHandler) {
var obj = {
InitData : function() {
var d = $q.defer(); // Set defer
StorageHandler.defaultConfig = {clientName:'test_feed'};
StorageHandler.prepData = function(data) {
var i = 0;
var maps = StorageHandler.dataMap;
i = data.line_up.length;
while(i--) {
// Do loads of string manipulations here
}
return data;
}
// Check for localdata
if(typeof StorageHandler.handle('localdata.favorites') == 'undefined') {
StorageHandler.handle('localdata.favorites',[]);
}
return d.promise; // Return promise
},
};
return obj;
});
But nothing is shown in console when I use this in my controller:
DataHandler.InitData()
.then(function () {
// Successful
console.log('success');
},
function () {
// failure
console.log('failure');
})
.then(function () {
// Like a Finally Clause
console.log('done');
});
Any thoughts?
Like Florian mentioned. Your asynchronous call is not obvious in the code you've shown.
Here is the gist of what you want:
angular.module("myApp",[]).factory("myFactory",function($http,$q){
return {
//$http.get returns a promise.
//which is latched onto and chained in the controller
initData: function(){
return $http.get("myurl").then(function(response){
var data = response.data;
//Do All your things...
return data;
},function(err){
//do stuff with the error..
return $q.reject(err);
//OR throw err;
//as mentioned below returning a new rejected promise is a slight anti-pattern,
//However, a practical use case could be that it would suppress logging,
//and allow specific throw/logging control where the service is implemented (controller)
});
}
}
}).controller("myCtrl",function(myFactory,$scope){
myFactory.initData().then(function(data){
$scope.myData = data;
},function(err){
//error loudly
$scope.error = err.message
})['finally'](function(){
//done.
});
});
More specifically is it possible to do something like:
authenticate a user
based on that user.id access a profile
profile determines authorization
update private information
1.then(2 or err).then(3 or err).then(4 or err) is straight forward.
I know there is $on('loaded', function(){ How do you access loaded data from .$on('loaded') in AngularFire 0.5.0 } unless it was already 'loaded' and still cached
and $on('change', function(){}).
Something like:
$scope.$on('$firebaseAuth:login', function(){
$scope.profile = $firebase(new..);
$scope.profile.$on('loaded', function(data){
if(data.authorized === true){
$scope.private = $firebase(new..);
$scope.private.$on('loaded', function(){
var foo = $scope.private.$child('foo');
foo = 'bar';
$scope.private.$save('foo');
$scope.private.someMethod(){..}
})
}
})
})
This isn't considering errors and doesn't seem like the cleanest way to do things. Is it possible to use promises with or instead of $on events, and if so how? If not, what is the best approach to this case that would catch any errors along the way?
My solution was to create services that return promises, take care of establishing the firebase references, and handle the $q & $on('loaded') stuff. I found this tutorial on chaining promises to be helpful.
.service('authorizeService',['$q', '$firebase',
function($q, $firebase){
"use strict";
return{
getIsAdmin: function(profile){
var dfrd = $q.defer();
var firef = $firebase(new Firebase('https://YOURFIREBASE.firebaseio.com/profiles/' + profile));
firef.$on('loaded',function(data){
if(data === null){
dfrd.reject();
}else{
dfrd.resolve({
displayName: data.displayName,
isAdmin: data.isAdmin
});
}
});
return dfrd.promise;
}
}
}])
.service('adminService',['$q',
function($q){
"use strict";
return{
getAdmin:function(admin){
var dfrd = $q.defer();
if(admin){
dfrd.resolve({
adminWidget: 'Behold... The Adminopacalypse.'
});
}else{
dfrd.resolve({
adminWidget: 'You are not an admin.'
});
}
return dfrd.promise;
}
};
}])
The controller contains functions that call those service methods attach values to the scope and return an object for the next promise in the chain. These were all wrapped in the $on('$firebase:login) to kick them off for each new login.
.controller('qtest', ['$rootScope','$scope', 'userQService', 'authorizeService', 'adminService', '$firebaseAuth',
function($rootScope,$scope, userQService, authorizeService, adminService, $firebaseAuth){
"use strict";
var loadAuthorizeAdmin = function(user){
return authorizeService.getIsAdmin(user)
.then(function(value){
$scope.isAdmin = value;
return value.isAdmin;
});
};
var loadAdmin = function(isAdmin){
return adminService.getAdmin(isAdmin)
.then(function(value){
$scope.adminWidget = value;
return value.adminWidget;
});
};
$rootScope.$on('$firebaseAuth:login',
function(){
var profile = $rootScope.auth.user.provider + '-' + $rootScope.auth.user.id;
$scope.user = profile;
loadAuthorizeAdmin(profile)
.then(loadAdmin);
//could be further chained
});
$rootScope.auth = $firebaseAuth(new Firebase('https://YOURFIREBASE.firebaseio.com'));
$scope.loginOne = function(){
$rootScope.auth.$login('password', {email: 'test#test.foo', password: 'test'});
}
$scope.loginTwo = function(){
$rootScope.auth.$login('password', {email: 'test2#test2.foo', password: 'test2'});
}
$scope.logout = function(){
$scope.user = null;
$scope.isAdmin = null;
$scope.adminWidget = null;
$rootScope.auth.$logout();
}
$scope.user = null;
$scope.isAdmin = null;
$scope.adminWidget = null;
}])
There is always a better approach but, this works. I didn't address errors but this is getting long.
I have this code:
var geocode = function(value) {
var request;
.....
var dResult = Q.defer();
geocoder.geocode(request, function (results) {
dResult.resolve(results);
});
return dResult.promise;
};
var cancelWatch;
$scope.$watch('value', function (value) {
$timeout.cancel(update);
update = $timeout(function () {
$scope.geocodedResult = geocode(value);
}, 300);
});
in line 15 $scope.geocodedResult is a promise that sooner or later will become the result value and the scope should refresh. This unfortunately does not happen.
The code works if I do
geocode(value).then(function(result) {
$scope.geocodedResult = result;
$scope.$digest();
});
What am I doing wrong?
UPDATE:
I'm now trying to use only $q but I cannot get it to work:
this.getCurrentPosition = function () {
var dCurrentPosition = $q.defer();
if (currentPosition) {
dCurrentPosition.resolve(currentPosition);
} else {
navigator.geolocation.getCurrentPosition(function (cp) {
currentPosition = cp;
dCurrentPosition.resolve(currentPosition);
});
}
return dCurrentPosition.promise;
};
this.getCurrentLoc = function () {
return self.getCurrentPosition().then(function (currentPosition) {
return [currentPosition.coords.longitude, currentPosition.coords.latitude];
});
};
a breakpoint in
return [currentPosition.coords.longitude, currentPosition.coords.latitude];
will never get triggered while it works fine with Q
If you use Angular's $q instead of Q, it should work:
var dResult = $q.defer();
$q promises are recognized by the templating engine in angular, which means that in templates you can treat promises attached to a scope as if they were the resulting values. -- $q docs
So $scope.geocodedResult can be set to the $q promise, and when the promise is resolved, the scope property will automatically update.