In the app I'm working on there is a situation in which data is pulled from a JSON file and is referenced in all subsequent routes. I want to ensure that a route does not load until this data is available, and if not available request it before loading the route. I'm trying to use a route resolve to accomplish this but am finding that the route will load regardless since the request to get the data returns a promise in the resolve. Here's an example of how the code for this is set up, is there a way to not load the route until the promise is resolved? I think the use of promises are throwing me off some.
Factory which pulls the data from the JSON file.
.factory('jsonFactory', function($q, $http) {
return {
getFormStuff: function() {
var deferred = $q.defer(),
httpPromise = $http.get('json/MasterObject.json');
httpPromise.then(function(response) {
deferred.resolve(response);
}, function(error) {
console.log(error);
});
return deferred.promise;
}
};
})
ngRoute config with resolve that checks if Model.formStuff is available and if not attempts to get it before loading the route which needs the data.
app.config(function ($routeProvider) {
$routeProvider.when('/someRoute', {
controller: 'someController',
templateUrl: 'views/someView.html',
resolve: {
getFormTemplate: function (Model, jsonFactory) {
if (!Model.formStuff) {
Model.formStuff = jsonFactory.getFormStuff();
return Model.formStuff;
} else {
return Model.formStuff;
}
}
}
})
EDIT: Adding the Model factory and controller where Model.formStuff is referenced. The Model.formStuff is dynamically added in a different controller and is not a pre-defined property...I inherited this code so not sure why it is handled like that.
angular.module('example', [])
.factory('Model', ['$resource',
function($resource) {
return {
query: function() {
return data;
},
get: function(id) {
return findById(id);
},
set: function(item) {
addItem(item);
},
put: function(item) {
updateItem(item);
},
del: function(id) {
removeItem(id);
},
getLoginUser: function(id) {
removeItem(id);
},
islogin: false
};
}
])
basic controller example showing how Model.formStuff is normally used.
angular.module(...)
.controller("someController", function(Model) {
$scope.someField = Model.formStuff[0].someProp;
var someVar = Model.formStuff.[0].otherProp;
// bunch of other code...
});
The code doesn't look that wrong. Please be sure to also handle the error case, otherwise the promise you return will never be rejected and the router will wait forever in case of some error. So you should call deferred.reject(error) in your error callback.
If you don't need any special processing on the data, you could directly return the promise of the $http.get() call like so:
getFormStuff = function() {
return $http.get('json/MasterObject.json');
}
What could possibly be the problem in your case is the controller definition. Do you inject a value named like the key in your resolve object into the controller? Otherwise the data will not be passed there...
angular.module(...)
.controller("someController", function(getFormTemplate) {
// do anything with the resolved data...
});
Another suggestion: Instead of handling the caching stuff directly in the resolve block, why not creating a special service that caches the data and just resolving like so:
resolve: {
getFormTemplate: function (MyCachingModel) {
return MyCachingModel.promiseToTemplate()
}
}
... and then moving the current logic into this caching service. This makes reasoning much clearer as your logic is not spread into the router config.
Related
I am making a web page with ui-router. Before we enter the controller, I want some operations to be done: 1) create a temporary folder in the server and write some files; 2) record the name of the folder and some other data. So naturally I choose to use resolve.
.state('panels', {
controller: 'PanelsCtrl',
resolve: {
init: ['codeService', function (codeService) {
return codeService.init()
}]
},
...
});
app.service('codeService', ['$http', function ($http) {
var srcP = "default" // private variable
this.getSrcP = function () { return srcP };
this.init = function () {
return $http.post("/writeFiles", ...) // write files in a temporary folder of the server
.then(function (res) {
srcP = res.data;
return srcP
}
}
};
app.controller('PanelsCtrl', ['$scope', 'codeService', 'init', function($scope, codeService, init) {
$scope.src = codeService.getSrcP();
...
}
The above code works. However, I feel odd about 1) I resolve an asynchronous function (ie, init) rather than data (that people usually resolve); 2) I use a side effect of init to record data (ie, srcP) in a service.
It seems that, in comparaison with resolving data, it is easier when we have more data to be recorded, we just need to have more private variables and make more side effects in the service.
Does anyone know if what I do is a bad/common/good practice? Additionally, why codeService in resolve: {...} and codeService injected to PanelsCtrl share same private variables?
I think that's a better approach if you clean up the service and don't resolve the promise inside it. You can let UI-ROUTER to resolve the promise and inject the result data in the controller...
.state('panels', {
controller: 'PanelsCtrl',
resolve: {
init: ['codeService', function (codeService) {
return codeService.init()
}]
}
});
app.service('codeService', ['$http', function ($http) {
this.init = function () {
return $http.post("/writeFiles");
}
};
app.controller('PanelsCtrl', ['$scope', 'init', function($scope, init) {
$scope.src = init; //init is the resolved data of codeService...
}
I've just made an example. Check this jsFiddle.
I think theoretically there's nothing wrong causing side effects in resolve, that's what it's there for. Take Restangular as an example. You keep calling for resources in resolve, the cacheing is handled by Restangular, which is basically a side effect.
However, I see some problems with the server-side approach. Storing files on the server is usually a bad idea (think about scaling/compromised immutability of the infrastructure). So I'd rather utilize the DB for this, and you could turn your folder abstraction to a DB resource.
I have Angular with ui-router, so toResolve variable will be resolved in my SomeController
.state('some.state', {
url: '/some',
controller: 'SomeController',
templateUrl: '/static/views/some-state.html',
resolve: {
toResolve: function(Resource) {
return Resource.get(function(response) {
return response;
});
},
But how to test this functionality with Jasmine? Let's suppose that I forget return statement, therefore the toResolve in my scope will be undefined.
Use services to make resolvers efficiently testable (and also mockable in integration/e2e tests).
Note: Angular services are singletons, state/route resolvers are not.
If caching a resolution is expected, a resolver may be moved to factory service.
app.factory('someResolver', function(Resource) {
return Resource.get(function(response) {
return response;
})
});
...
resolve: { toResolve: 'someResolver' },
On the other hand, if the resolver is expected to be evaluated on each route change, this may lead to undesirable app behaviour. In this case the appropriate recipe may be constant annotated function:
app.constant('someResolver', ['Resource', function(Resource) {
return Resource.get(function(response) {
return response;
})
}]);
app.config(function (someResolver, ...) {
...
resolve: { toResolve: someResolver },
...
Otherwise the specs may end encumbered with a pile of boilerplate code:
var toResolveFactory = $state.get('some.state').resolve.toResolve;
var toResolve = $injector.invoke(toResolveFactory);
One good article on related topic is http://nikas.praninskas.com/angular/2014/09/27/unit-testing-ui-router-configuration/, but it's not the solution. One idea is to pass another object to resolve and then test that unit of code separately which is good when you're resolving a bunch of items.
I'm just starting out in angular, and i'm building a simple item management app that loads items from a json file, and shows the items in a list view. I'll be allowing the user to edit and create new data as well, but i can't seem to get past the first step.
When I load the data directly in my list controller, It works just fine. While reading up on best practices, it seems like you shouldn't communicate directly with a json file in your controller, but rather handle these things within a factory (let me know if i'm mistaken). I can't get it to work though. Here's my code:
var app = angular.module('itemsApp', ['ngRoute']);
app.config(function($routeProvider) {
$routeProvider
.when('/', {
controller:'ListCtrl',
templateUrl:'list.html'
})
.when('/edit/:itemId', {
controller:'EditCtrl',
templateUrl:'detail.html'
})
.when('/new', {
controller:'CreateCtrl',
templateUrl:'detail.html'
})
.otherwise({
redirectTo:'/'
});
})
app.factory('ItemsFactory',function($http){
return {
getItems: function() {
return $http.get('js/items.json')
.then(function(res){
return res.data;
});
}
};
});
app.controller('ListCtrl', function($scope, $http, ItemsFactory) {
$http.get('js/items.json')
.then(function(res){
$scope.items = res.data;
});
});
The controller works fine as i have it here, however, when i try to just set $scope.items to the result of ItemsFactory.getItems();, I get nothing. Any ideas why?
Returning inside the then promise method doesn't return any to the caller of getItems (like in any other callback). I suggest you to manage that kind of situation in this way:
app.factory('ItemsFactory',function($http){
return {
getItems: function() {
return $http.get('js/items.json');
}
};
});
app.controller('ListCtrl', function($scope, ItemsFactory) {
ItemsFactory.getItems().then(function(res){
$scope.items = res.data;
});
});
Hope it helps.
Dario
The best practices you are reading are absolutely right and you should have all the server communications inside one seperate module which will just have all the factories doing server exchange there.
Then you can just inject this, let's say integration module and all the factories required for server communctions become available.
Now, a sample factory can be like:
angular.module('integrationModule')
.factory('ItemsFactory',function($http){
return {
getItems: function() {
return $http.get('js/items.json');
}
};
});
app.controller('ListCtrl', function($scope, ItemsFactory) {
ItemsFactory.getItems().then(function(res){
$scope.items = res.data;
});
});
//credits Dario
or this is my preferred way(no promises needed here)
angular.module('integrationModule')
.factory('getLocaleProperties', ['$http', function($http) {
return function(requestObj, callBackFunc){
console.log('#getLocaleProperties:');
console.log(requestObj);
$http.get('assets/locale/localeProperties.json')
.success(function(data) {
callBackFunc(data);
})
.error(function(){
console.log('error in get Locale properties');
});
}
}])
Now if you notice, I am passing a callback function which is executed only when the $http call is successful, so in controllers you can inject this factory and pass it some function like:
getLocaleProperties({//some request object when posting/as query params in url}, function(data){
//do some operation on the data
})
This way I can call the factory from different controllers and perform different actions when the call is successful. Just I need to pass different callback functions.
You can also use promise, like the way you are doing, but they are only needed when you want a synchronous call like behaviour, rest can be done be callbacks.($http call is specified async in angular source code).
Hope this helps.
Update: this should be possible in angular-ui-router as of 1.0.0alpha0. See the release notes https://github.com/angular-ui/ui-router/releases/tag/1.0.0alpha0 and the issue https://github.com/angular-ui/ui-router/issues/1018 I created.
I would like to access the state's name and other attributes the app is navigating to using angular ui-router when working on the resolve.
The reason: I want load some user data (including their access rights) asynchronously before allowing the app the enter that page.
Currently this is not possible because injecting $state into the resolve points to the state you're navigating away form, not to the one you're navigating to.
I know I can:
get the toState somewhere else with $rootScope('$stateChangeStart') and save it in my settings service for instance. But I think it's a little messy.
hard code the state into the resolve, but I don't want to reuse my resolve for all pages
I also created an issue on the ui-router github (Please + 1 if you are interested!):
https://github.com/angular-ui/ui-router/issues/1018
Here's my code so far. Any help appreciated!
.config(function($stateProvider) {
$stateProvider.state('somePage', {
// ..
resolve: {
userData: function($stateParams, $state, Settings) {
return Settings.getUserData() // load user data asynchronously
.then(function (userData) {
console.log($stateParams);
console.log($state);
// Problem: $state still points to the state you're navigating away from
});
}
}
});
});
Update for Ui-Router 1.x
$provide.decorator('$state', ($delegate, $transitions) => {
$transitions.onStart({}, (trans) => {
$delegate.toParams = trans.params()
$delegate.next = trans.to().name
})
return $delegate
})
Ui-Router 0.x
You can always decorate $state with next and toParams properties:
angular.config(function($provide) {
$provide.decorator('$state', function($delegate, $rootScope) {
$rootScope.$on('$stateChangeStart', function(event, state, params) {
$delegate.next = state;
$delegate.toParams = params;
});
return $delegate;
});
});
And use as such:
.state('myState', {
url: '/something/{id}',
resolve: {
oneThing: function($state) {
console.log($state.toParams, $state.next);
}
}
});
So I discovered the answer to this myself. If you're code is behaving like mine, the $stateParams object is properly injected, but $state is an empty (or old) state object.
What worked for me was referencing this in the resolve function:
.state('myState', {
url: '/something/{id}',
templateUrl: '/myTemplate.html',
controller: function() {},
resolve: {
oneThing: function($stateParams) {
console.log($stateParams); // comes through fine
var state = this;
console.log(state); // will give you a "raw" state object
}
}
})
The first log will return what you'd expect. The second log will return a "raw" (for lack of a better term) state object. So, for instance, to get the state's name, you can access, this.self.name.
I realize this isn't preferred...it would be a lot nicer if $state (or another standardized object) could provide this information for us at the resolve, but this is the best I could find.
Hope that helps...
this.toString() will give you the state name
This has been asked here.
It looks like they built into 1.0.0-rc.2 $state$ which you can inject into the resolve function and get this information.
resolve: {
oneThing: function($state$) {
console.log($state$);
}
}
A bit of info. I'm working on a single page app, but am attempting to make it just an HTML file, rather than an actual dynamic page that contains all the bootstrap information in it. I'm also hoping to, when the app boots (or perhaps prior to), check to see if the current session is 'logged in', and if not then direct the hash to the 'login'.
I'm new to Angular, and am having a difficult time figuring out how to program out this flow. So, in essence..
HTML page loaded with 'deferred' bootstrap
Hit URL to get login status
If status is 'not logged in', direct to #/login
Start app
Any pointers on where #2 and #3 would live? In my 'easy world' I'd just use jquery to grab that data, and then call the angular.resumeBootstrap([appname]). But, as I'm trying to actually learn Angular rather than just hack around the parts I don't understand, I'd like to know what would be used in this place. I was looking at providers, but I'm not sure that's what I need.
Thanks!
EDIT
Based on #Mik378's answer, I've updated my code to the following as a test. It works to a point, but as the 'get' is async, it allows the application to continue loading whatever it was before then shooting off the status results..
var app = angular.module('ping', [
'ngRoute',
'ping.controllers'
]).provider('security', function() {
this.$get = ['$http', function($http) {
var service = {
getLoginStatus: function () {
if (service.isAuthenticated())
return $q.when(service.currentUser);
else
return $http.get('/login/status').then(function (response) {
console.log(response);
service.loggedIn = response.data.loggedIn;
console.log(service);
return service.currentUser;
});
},
isAuthenticated: function () {
return !!service.loggedIn;
}
};
return service;
}];
}).run(['security', function(security) {
return security.getLoginStatus().then(function () {
if(!security.isAuthenticated()) {
console.log("BADNESS");
} else {
console.log("GOODNESS");
}
});
}]);
My hope was that this could somehow be completed prior to the first controller booting up so that it wasn't loading (or attempting to load) things that weren't even cleared for access yet.
EDIT #2
I started looking into the 'resolve' property in the router, and #Mik378 verified what I was looking at. My final code that is (currently) working how I want it is as follows (appologies about the super long code block)
angular.module('ping.controllers', [])
.controller('Dashboard', ['$scope', function($scope) {
console.log('dashboard')
}])
.controller('Login', ['$scope', function($scope) {
console.log('login')
}]);
var app = angular.module('ping', [
'ngRoute',
'ping.controllers'
]).run(['$rootScope', '$location', function($root, $location) {
$root.$on("$routeChangeError", function (event, current, previous, rejection) {
switch(rejection) {
case "not logged in":
$location.path("/login"); //<-- NOTE #1
break;
}
});
}]);
app.provider('loginSecurity', function() {
this.$get = ['$http', '$q', function($http, $q) {
var service = {
defer: $q.defer, //<-- NOTE #2
requireAuth: function() { //<-- NOTE #3
var deferred = service.defer();
service.getLoginStatus().then(function() {
if (!service.isAuthenticated()) {
deferred.reject("not logged in")
} else {
deferred.resolve("Auth OK")
}
});
return deferred.promise;
},
getLoginStatus: function() {
if (service.isAuthenticated()) {
return $q.when(service.currentUser);
} else {
return $http.get('/login/status').then(function(response) {
console.log(response);
service.loggedIn = response.data.loggedIn;
console.log(service);
return service.currentUser;
});
}
},
isAuthenticated: function() {
return !!service.loggedIn;
}
};
return service;
}
];
});
app.config(['$routeProvider', function($routeProvider) {
console.log('Routing loading');
$routeProvider.when('/', {
templateUrl: 'static/scripts/dashboard/template.html',
controller: 'Dashboard',
resolve: {'loginSecurity': function (loginSecurity) {
return loginSecurity.requireAuth(); //<- NOTE #4
}}
});
$routeProvider.when('/login', {
templateUrl: 'static/scripts/login/template.html',
controller: 'Login'
});
$routeProvider.otherwise({redirectTo: '/404'});
}]);
Notes:
This section hooks into routing failures. In the case of a "no login", I wanted to catch the failure and pop the person over to the login page.
I can't get access to the $q inside of the requireAuth function, so I grabbed a reference to it. Perhaps a better way of doing this exists?
This function wraps up the other two - it uses the promise returned from getLoginStatus, but returns its own promise that will be rejected if the end result from the getLoginStatus winds up with the user not being logged in. Sort of a round-about way of doing it.
This returns #3's promise, which is used by the $routeProvider.. so if it fails, the routing fails and you end up catching it at #1.
Whew. I think that's enough for a day. Time for a beer.
No need to use a deferred bootstrap for your case:
angular.module('app').run(['security', '$location', function(security) {
// Get the current user when the application starts
// (in case they are still logged in from a previous session)
security.requestCurrentUser().then(function(){
if(!security.isAuthenticated())
$location.path('yourPathToLoginPage')
}; //service returning the current user, if already logged in
}]);
this method requestCurrentUser would be the following:
requestCurrentUser: function () {
if (service.isAuthenticated())
return $q.when(service.currentUser);
else
return $http.get('/api/current-user').then(function (response) {
service.currentUser = response.data.user;
return service.currentUser;
});
}
and inside security service again:
isAuthenticated: function () {
return !!service.currentUser;
}
Note the run method of the module => As soon as the application runs, this service is called.
-- UPDATE --
To prevent any controller to be initialized before the promise provided by requestCurrentUser is resolved, a better solution, as evoked in the comments below, is to use the resolve route property .