The first time I visit a route with a resolve the request for the objects is not sent. The only way to visit the page is to make sure the route is correct in the url bar (typing or clicking a link) and refresh the page without caching (ctrl+shift+r in Firefox or ctrl+F5 in Chrome).
After I visit it the first time link will work.
app.config(['$stateProvider', function($stateProvider){
$stateProvider.state('users', {
templateUrl: '/app/Users/templates/users.html',
controller: 'Users',
resolve: {
'users': function(Objects, $stateParams){
return Objects.getUsers();
}
},
url: '^/users'
});
$stateProvider.state('user', {
templateUrl: '/app/Users/templates/user.html',
controller: 'User',
resolve: {
'user': function(Objects, $stateParams){
return Objects.getUser($stateParams.id);
}
},
url: '^/users/:id/'
});
}]);
app.factory('Objects', ['$http', '$q', function($http, $q){
/* Retrieve objects once */
var _cache = {};
function cache(key, promiseGetterFn) {
if (key in _cache) {
return _cache[key];
}
else {
var promise = promiseGetterFn();
_cache[key] = promise;
return promise;
}
}
return {
unsetKey: function(key){
delete _cache[key];
},
getUsers: function() {
return cache('users', function () {
var deferred = $q.defer();
$http.get(HOST + '/api/v1.0/users/all').then(
function (result) {
deferred.resolve(result);
});
return deferred.promise;
});
},
/*
getUsers: function(){
return cache('users', function(){
return $http.get(HOST + '/api/v1.0/users/all').success(
function(data, status, headers, config){
return data.users;
}
);
});
},
*/
/*
getUsers: function(){
return cache('users', function(){
var deferred = $q.defer();
return $http.get(HOST + '/api/v1.0/users/all').then(
function(result){
deferred.resolve(result.data.users);
},
function(status){
deferred.reject(status);
}
);
return deferred.promise;
});
},
*/
getUser: function(id){
return cache('user_' + id, function(){
var deferred = $q.defer();
return $http.get(HOST + '/api/v1.0/user/' + id).then(
function(result){
deferred.resolve(result.data.user);
},
function(status){
deferred.reject(status);
}
);
return deferred.promise;
});
},
};
}]);
app.run(['$rootScope', '$location', 'LocalService', function($rootScope, $location, LocalService){
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
if (!toState.publicAccess && !LocalService.get('loggedIn')){
/* Store the route they were trying to access */
LocalService.set('next', $location.path());
$location.path('/login');
}
});
}]);
Redirect after login code
app.factory('AuthInterceptor', ['$q', '$injector', '$location', 'LocalService', function($q, $injector, $location, LocalService){
/* Send Authorization in the header of each http request if there is a token */
return {
request: function(config){
if (LocalService.get('token')){
/* Using btoa to do Base64 */
/* LocalService.password is only used on login to get token and will be empty ('') when using the token */
config.headers.Authorization = 'Basic ' + btoa(LocalService.get('token') + ':' + LocalService.get('password'));
}
return config;
},
responseError: function(response){
if(response.status === 401 || response.status === 403){
/* Log the user out */
LocalService.unset('loggedIn');
LocalService.unset('token');
LocalService.unset('user');
$location.path('/login');
}
return $q.reject(response);
}
};
}]);
app.config(['$httpProvider', function($httpProvider){
$httpProvider.interceptors.push('AuthInterceptor');
}]);
app.run(['$rootScope', '$location', 'LocalService', function($rootScope, $location, LocalService){
$rootScope.$on('$stateChangeStart', function(event, toState, toParams, fromState, fromParams){
if (!toState.publicAccess && !LocalService.get('loggedIn')){
/* Store the route they were trying to access */
LocalService.set('next', $location.path());
$location.path('/login');
}
});
}]);
app.controller('Login', ['$scope', '$http', '$location', 'growl', 'LocalService',
function($scope, $http, $location, growl, LocalService){
$scope.email = '';
$scope.password = '';
$scope.submitLogin = function submitLogin(){
LocalService.set('token', $scope.email);
LocalService.set('password', $scope.password);
$http.get(HOST + '/api/v1.0/token').
success(function(data, status, headers, config) {
LocalService.set('token', data.token);
LocalService.set('loggedIn', true);
LocalService.set('password', '');
/* Set current user */
$http.get(HOST + '/api/v1.0/authenticate').then(function(result) {
LocalService.set('user', JSON.stringify(result.data));
if (LocalService.get('next')){
var next = LocalService.get('next');
LocalService.unset('next');
console.log(next);
$location.path(next);
}
else{
$location.path('/');
}
});
}
).
error(function(data, status, headers, config) {
/* invalid credentials growl */
growl.addErrorMessage('Invalid username or password.');
}
);
};
}
]);
My first thought on this would be that the resolved objects are only resolved on hard load. Once the app has been instantiated from an index.html, the partial views may not be registering that the objects are promises. I would try making the returned objects from the factory (the api) actual promises. Now I have never seen a promise quite like yours, but I do not see any 'deferred.resolve' or a 'deffered.reject'. For instance:
return {
getBundles: function(){
return cache('bundles', function(){
var deffered = $q.defer
return $http.get(HOST + '/api/v1.0/bundles/all').success(
function(data, status, headers, config){
deferred.resolve(data.bundles);
}.error(function(status){
deferred.reject(status);
)};
return deferred.promise;
});
},
}
In doing this, I would also recommend that you return the objects to a javascript object before binding it to the view. This would be done in the controller with the 'bundles' injected into the controller.
var thisBundle = bundles.data.bundles;
thisBundle.then(function(data){
$scope.bundles = data;
});
Another solution I found was to wrap all of your items with a resolve in the routing. Try this:
resolve: {
'allProducts' : function(){
var theResolvePromise = $q.defer();
theResolvePromise.resolve({
bundles: ['Objects', function(Objects){
return Objects.getBundles();
}],
products: ['Objects', function(Objects){
return Objects.getProducts();
}],
technologies: ['Objects', function(Objects){
return Objects.getTechnologies();
}],
deliveryCategories: ['Objects', function(Objects){
return Objects.getDeliveryCategories();
}],
});
return theResolvePromise.promise;
};
}
}).
Then you would access this in the controller with the params passed through.
Retrieved from: http://www.undefinednull.com/2014/02/17/resolve-in-angularjs-routes-explained-as-story/
Hope this helps,
Patrick
Try this. I didn't test it, so it might not be perfect, but maybe it will help point you in the right direction.
app.factory('Objects', ['$http', function($http){
var _cache = {};
function cache(key, getterFn) {
if (_cache[key] == null) {
_cache[key] = getterFn();
_cache[key].then(function (result) {
_cache[key] = result;
});
}
return _cache[key];
}
return {
unsetKey: function(key){
delete _cache[key];
},
getUsers: function() {
return cache('users', function () {
return $http.get(HOST + '/api/v1.0/users/all');
});
},
getUser: function(id){
return cache('user_' + id, function() {
return $http.get(HOST + '/api/v1.0/user/' + id).then(function (result) {
return result.data.user;
});
});
},
};
}]);
$http.get returns a promise that resolves when the request completes. Returning a value from a .then() function will pop that value into the next .then() in the chain. UI-Router's resolves automatically unwrap the promise if given one. If given anything else, it just returns it. Promises don't automatically unwrap in Angular 1.2+ so you need to unwrap it yourself when you're working in your cache.
Your question is 2 part.
1) Why my first resolve not resolving the resolve for the state ?
The first time you return a "promise to fulfill a promise of fulfilling data". It is like a chained promise. It will take one extra $digest cycle to resolve this vs. if you returned a promise to get the data. $http.get() returns a promise.
In AngularJS the results of promise resolution are propagated asynchronously, inside a $digest cycle. So, callbacks registered with then() will only be called upon entering a $digest cycle.
Also check this out
Angular JS: Chaining promises and the digest cycle
2) What can you change to fix the problem ?
You can do 2 things
Just return the $http.get() because this is itself a promise.
Why cache a promise object ? You should cache the actual data.
Also with $resource and $http you can pass {cache: true} and the results will get cached. Alternatively if you like to take control you may want to use $cacheFactory. In this case you can place the actual results in the cache and not the promise object.
Related
I have a factory that request user data via an api call:
angular.module('MyApp')
.factory('UserApi', function($auth,Account){
return {
getProfile: function()
{
Account.getProfile()
.then(function(response){
return response.data; ----> returning json data ok!!
});
}
}
});
But when i call the function in controller, it return me undefined
myApp.controller('AppCtrl', function($rootScope,$state,$window,$document,$scope,$filter,$resource,cfpLoadingBar,$translate,UserApi){
$scope.user = function(){
UserApi.getProfile().then(function(data){
$scope.currentUser = data;
})
}
console.log($scope.user()); ----> undefined
});
account factory:
angular.module('MyApp')
.factory('Account', function($http){
return {
getProfile: function(){
return $http.get('/api/me');
}
}
});
The error that log in console is TypeError: Cannot read property 'then' of undefined
EDIT
The only available solution is to set the response.data to $rootScope.user in which the data will be available across the controllers.
angular.module('MyApp')
.factory('UserApi', function($auth,Account,$rootScope){
return {
getProfile: function()
{
Account.getProfile()
.then(function(response){
$rootScope.user = response.data; ----> returning json data ok!!
});
return $rootScope.user;
}
}
});
First of all getProfile method should return a promise (not undefined like it's doing in your code):
angular.module('MyApp')
.factory('UserApi', function($auth,Account){
return {
getProfile: function()
{
return Account.getProfile()
.then(function(response) {
return response.data;
});
}
}
});
then in controller you should use then callback:
myApp.controller('AppCtrl', function ($rootScope, $state, $window, $document, $scope, $filter, $resource, cfpLoadingBar, $translate, UserApi) {
$scope.user = function () {
UserApi.getProfile().then(function (data) {
$scope.currentUser = data;
console.log($scope.currentUser);
})
};
});
Also make sure you understand the difference between synchronous and asynchronous code, and why console.log($scope.user()) makes no sense in this case: response if not yet available when you try to log it. Instead you provide a callback to be called when data has come.
You are trying to return the data after the request is completed successfully. But, since this is an ajax call we don't know when it will be completed (Basically, runs on a different thread.). There are two ways you can resolve this.
1 - Just return the call like so.
angular.module('MyApp')
.factory('UserApi', function($auth,Account){
return {
getProfile: function(){
return Account.getProfile(); // return call and resolve in controller.
}
}
});
2 - You can use promise ($q)
angular.module('MyApp')
.factory('UserApi', function($auth,Account, $q){
return {
getProfile: function(){
var deferred = $q.defer();
Account.getProfile()
.success(function(data){
deferred.resolve(data);
});
return deferred.promise; // just return the promise
}
}
});
and in your controller just have the following.
myApp.controller('AppCtrl', function($rootScope,$state,$window,$document,$scope,$filter,$resource,cfpLoadingBar,$translate,UserApi){
$scope.user = function(){
UserApi.getProfile().then(function(data){
$scope.currentUser = data;
console.log($scope.currentUser);
});
}
});
EDITED:
You get undefined. Because:
there isn't return in $scope.user
your console.log($scope.user($scope.user()) works only initial time.
there is time delay for getting data from UserApi.getProfile()
Also, your codes have some mistakes:
I can suggest that:
Don't use your console.log($scope.user()) initial time.
Or, You should get all data initial time when factory created. Then, you use UserApi.data in your controller.(But, there is time delay. You may get success data, if request return response before from loading of your controller).
.
angular.module('MyApp')
.factory('UserApi', function ($auth, Account) {
var data;
Account.getProfile().then(function (response) {
data = response.data;
});
return {
data: data
}
});
myApp.controller('AppCtrl', function ($rootScope, $state, $window, $document, $scope, $filter, $resource, cfpLoadingBar, $translate, UserApi) {
console.log(UserApi.data);
});
I have an app that when you select a project, it goes into the project section where it needs to load all the information and data about a project asynchronously.
I wanted to store all the data in a singleton service so I can access the data in all the project's subsections(project header, project footer, main menu, etc)
If user clicks a different project, it will need to re-initialize with different URL parameter (in this case, project_id).
app.factory('ProjectService', function($http, project_id) {
var SERVICE = {
async: function() {
var promise = $http.get('SOME URL' + project_id).then(function(response) {
return response.data;
});
return promise;
}
};
return SERVICE;
});
What is the best way to achieve this and how can I reinitialize the service with different URL parameters when user clicks a button?
Check working demo: JSFiddle
First of all, using a factory may be more suitable for your case.
You need to play with the deferred/promise manually. If the requested id is already loaded, resolve the deferred object immediately. Otherwise, send a HTTP request (in the demo I just used an public API providing fake data) and fetch the project information.
app.factory('ProjectFactory', ['$http', '$q', function ($http, $q) {
var myProject;
return {
project: function (id) {
var deferred = $q.defer();
// If the requested id is fetched already, just resolve
if (!id || (myProject && myProject.id === id)) {
console.log('get from cache');
deferred.resolve(myProject);
} else {
console.log('sending request...');
$http.get('http://jsonplaceholder.typicode.com/posts/' + id).success(function (response) {
myProject = response;
deferred.resolve(myProject);
}).error(function (response) {
deferred.reject(response);
});
}
return deferred.promise;
}
};
}]);
To use this factory:
app.controller('JoyCtrl', ['$scope', '$timeout', 'ProjectFactory', function ($scope, $timeout, ProjectFactory) {
ProjectFactory.project(1).then(function (project) {
$scope.project = project;
ProjectFactory.project(1).then(function (project) {
});
}, function (reason) {
console.log('Failed: ' + reason);
});
}]);
For your reference: $http, $q
I have two directives, each consuming the same factory wrapping a $q/$http call.
angular.module("demo").directive("itemA", ["restService", function(restService) {
return {
restrict: "A",
link: function(scope, element, attrs) {
restService.get().then(function(response) {
// whatever
}, function(response) {
// whatever
});
}
};
}]);
angular.module("demo").directive("itemB", ["restService", function(restService) {
return {
restrict: "A",
link: function(scope, element, attrs) {
restService.get().then(function(response) {
// whatever
}, function(response) {
// whatever
});
}
};
}]);
angular.module("demo").factory("restService", ["$http", "$q", function($http, $q) {
return {
get: function() {
var dfd = $q.defer();
$http.get("whatever.json", {
cache: true
}).success(function(response) {
// do some stuff here
dfd.resolve(response);
}).error(function(response) {
// do some stuff here
dfd.reject(response);
});
}
};
}]);
Problem: When I do this
<div item-a></div>
<div item-b></div>
I get the same web service fired off twice, because the GET from ItemA is still in progress when the GET for ItemB goes.
Is there a way for whichever fires second to know that there's already a request to this in progress, so that it can wait a minute and grab it for free?
I've thought about making an $http or $q wrapper which flags each URL as pending or not but I'm not sure that's the best way. What would I do if it was pending? Just return the existing promise and it'll resolve when the other resolves?
Yes, all you need to do is to cache the promise and clean it off after the request is done. Any subsequent request in between can just use the same promise.
angular.module("demo").factory("restService", ["$http", "$q", function($http, $q) {
var _cache;
return {
get: function() {
//If a call is already going on just return the same promise, else make the call and set the promise to _cache
return _cache || _cache = $http.get("whatever.json", {
cache: true
}).then(function(response) {
// do some stuff here
return response.data;
}).catch(function(response) {
return $q.reject(response.data);
}).finally(function(){
_cache = null; //Just remove it here
});
}
};
}]);
Please go through the code first
app.js
var app = angular.module('Nimbus', ['ngRoute']);
route.js
app.config(function($routeProvider) {
$routeProvider
.when('/login', {
controller: 'LoginController',
templateUrl: 'templates/pages/login.html',
title: 'Login'
})
.when('/home', {
controller: 'HomeController',
templateUrl: 'templates/pages/home.html',
title: 'Dashboard'
})
.when('/stats', {
controller: 'StatsController',
templateUrl: 'templates/pages/stats.html',
title: 'Stats'
})
}).run( function($q, $rootScope, $location, $route, Auth) {
$rootScope.$on( "$routeChangeStart", function(event, next, current) {
console.log("Started");
/* this line not working */
var canceler = $q.defer();
canceler.resolve();
});
$rootScope.$on("$routeChangeSuccess", function(currentRoute, previousRoute){
$rootScope.title = ($route.current.title) ? $route.current.title : 'Welcome';
});
})
home-controller.js
app.controller('HomeController',
function HomeController($scope, API) {
API.all(function(response){
console.log(response);
})
}
)
stats-controller.js
app.controller('StatsController',
function StatsController($scope, API) {
API.all(function(response){
console.log(response);
})
}
)
api.js
app.factory('API', ['$q','$http', function($q, $http) {
return {
all: function(callback) {
var canceler = $q.defer();
var apiurl = 'some_url'
$http.get(apiurl,{timeout: canceler.promise}).success(callback);
}
}
}]);
When I move from home to stats , again API will send http request, I have many http calls like this, I pasted only few lines of code.
What I need is I need to cancel abort all pending http requests on routechangestart or success
Or any other way to implement the same ?
I put together some conceptual code for this. It might need tweaking to fit your needs. There's a pendingRequests service that has an API for adding, getting and cancelling requests, a httpService that wraps $http and makes sure all requests are tracked.
By leveraging the $http config object (docs) we can get a way to cancel a pending request.
I've made a plnkr, but you're going to need quick fingers to see requests getting cancelled since the test-site I found typically responds within half a second, but you will see in the devtools network tab that requests do get cancelled. In your case, you would obviously trigger the cancelAll() call on the appropriate events from $routeProvider.
The controller is just there to demonstrate the concept.
DEMO
angular.module('app', [])
// This service keeps track of pending requests
.service('pendingRequests', function() {
var pending = [];
this.get = function() {
return pending;
};
this.add = function(request) {
pending.push(request);
};
this.remove = function(request) {
pending = _.filter(pending, function(p) {
return p.url !== request;
});
};
this.cancelAll = function() {
angular.forEach(pending, function(p) {
p.canceller.resolve();
});
pending.length = 0;
};
})
// This service wraps $http to make sure pending requests are tracked
.service('httpService', ['$http', '$q', 'pendingRequests', function($http, $q, pendingRequests) {
this.get = function(url) {
var canceller = $q.defer();
pendingRequests.add({
url: url,
canceller: canceller
});
//Request gets cancelled if the timeout-promise is resolved
var requestPromise = $http.get(url, { timeout: canceller.promise });
//Once a request has failed or succeeded, remove it from the pending list
requestPromise.finally(function() {
pendingRequests.remove(url);
});
return requestPromise;
}
}])
// The controller just helps generate requests and keep a visual track of pending ones
.controller('AppCtrl', ['$scope', 'httpService', 'pendingRequests', function($scope, httpService, pendingRequests) {
$scope.requests = [];
$scope.$watch(function() {
return pendingRequests.get();
}, function(pending) {
$scope.requests = pending;
})
var counter = 1;
$scope.addRequests = function() {
for (var i = 0, l = 9; i < l; i++) {
httpService.get('https://public.opencpu.org/ocpu/library/?foo=' + counter++);
}
};
$scope.cancelAll = function() {
pendingRequests.cancelAll();
}
}]);
You can use $http.pendingRequests to do that.
First, when you make request, do this:
var cancel = $q.defer();
var request = {
method: method,
url: requestUrl,
data: data,
timeout: cancel.promise, // cancel promise, standard thing in $http request
cancel: cancel // this is where we do our magic
};
$http(request).then(.....);
Now, we cancel all our pending requests in $routeChangeStart
$rootScope.$on('$routeChangeStart', function (event, next, current) {
$http.pendingRequests.forEach(function(request) {
if (request.cancel) {
request.cancel.resolve();
}
});
});
This way you can also 'protect' certain request from being cancelled by simply not providing 'cancel' field in request.
I think this is the best solution to abort requests. It's using an interceptor and $routeChangeSuccess event.
http://blog.xebia.com/cancelling-http-requests-for-fun-and-profit/
Please notice that im new with Angular so this may not be optimal.
Another solution could be:
on the $http request adding the "timeout" argument, Docs I did it this way:
In a factory where I call all my Rest services, have this logic.
module.factory('myactory', ['$http', '$q', function ($http, $q) {
var canceler = $q.defer();
var urlBase = '/api/blabla';
var factory = {};
factory.CANCEL_REQUESTS = function () {
canceler.resolve();
this.ENABLE_REQUESTS();
};
factory.ENABLE_REQUESTS = function () {
canceler = $q.defer();
};
factory.myMethod = function () {
return $http.get(urlBase, {timeout: canceler.promise});
};
factory.myOtherMethod= function () {
return $http.post(urlBase, {a:a, b:b}, {timeout: canceler.promise});
};
return factory;
}]);
and on the angular app configuration I have:
return angular.module('app', ['ngRoute', 'ngSanitize', 'app.controllers', 'app.factories',
'app.filters', 'app.directives', 'ui.bootstrap', 'ngGeolocation', 'ui.select' ])
.run(['$location', '$rootScope', 'myFactory', function($location, $rootScope, myFactory) {
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
myFactory.CANCEL_REQUESTS();
$rootScope.title = current.$$route.title;
});
}]);
This way it catches all the "route" changes and stops all the request configured with that "timer" so you can select what is critical for you.
I hope it helps to someone.
Regards
Im writing some unit tests for my controller which uses promises.
Basically this:
UserService.getUser($routeParams.contactId).then(function (data) {
$scope.$apply(function () {
$scope.contacts = data;
});
});
I have mocked my UserService. This is my unit test:
beforeEach(inject(function ($rootScope, $controller, $q, $routeParams) {
$routeParams.contactId = contactId;
window.localStorage.clear();
UserService = {
getUser: function () {
def = $q.defer();
return def.promise;
}
};
spyOn(UserService, 'getUser').andCallThrough();
scope = $rootScope.$new();
ctrl = $controller('ContactDetailController', {
$scope: scope,
UserService:UserService
});
}));
it('should return 1 contact', function () {
expect(scope.contacts).not.toBeDefined();
def.resolve(contact);
scope.$apply();
expect(scope.contacts.surname).toEqual('NAME');
expect(scope.contacts.email).toEqual('EMAIL');
});
This give me the following error:
Error: [$rootScope:inprog] $digest already in progress
Now removing the $scope.$apply in the controller causes the test to pass, like this:
UserService.getUser($routeParams.contactId).then(function (data) {
$scope.contacts = data;
});
However this breaks functionality of my controller... So what should I do here?
Thanks for the replies, the $apply is not happening in the UserService. It's in the controller. Like this:
EDIT:
The $apply is happening in the controller like this.
appController.controller('ContactDetailController', function ($scope, $routeParams, UserService) {
UserService.getUser($routeParams.contactId).then(function (data) {
$scope.$apply(function () {
$scope.contacts = data;
});
});
Real UserService:
function getUser(user) {
if (user === undefined) {
user = getUserId();
}
var deferred = Q.defer();
$http({
method: 'GET',
url: BASE_URL + '/users/' + user
}).success(function (user) {
deferred.resolve(user);
});
return deferred.promise;
}
There are a couple of issues in your UserService.
You're using Q, rather than $q. Hard to know exactly what effect this has, other than it's not typical when using Angular and might have affects with regards to exactly when then callbacks run.
You're actually creating a promise in getUser when you don't really need to (can be seen as an anti-pattern). The success function of the promise returned from $http promise I think is often more trouble than it's worth. In my experience, usually better to just use the standard then function, as then you can return a post-processed value for it and use standard promise chaining:
function getUser(user) {
if (user === undefined) {
user = getUserId();
}
return $http({
method: 'GET',
url: BASE_URL + '/users/' + user
}).then(function(response) {
return response.data;
});
}
Once the above is changed, the controller code can be changed to
UserService.getUser($routeParams.contactId).then(function (data) {
$scope.contacts = data;
});
Then in the test, after resolving the promise call $apply.
def.resolve(contact);
scope.$apply();