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.
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.
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.
I there,
I'm building an Angular.js app with the help of Restangular and angular-local-storage. I need to retrieve some data from a RESTFull service server and assign it to a $scope variable.
I would like to know how could I wait for all that data to load before loading it to my view (html).
Here's what I've done so far:
app.controller('InventoryController', function($scope, inventoryService) {
$scope.productList = inventoryService.getProduces();
console.log($scope.productList); // At that point, $scope.productList is null
});
app.service('inventoryService', function(entityService, browserStorageService) {
entityService.allUrl('product', entityService.getBaseUrl).getList().then(function(data){
console.log(data); // The data variable is not null here.
browserStorageService.set('producList', data);
});
this.getProduces = function() {
return browserStorageService.get('producList');
};
});
app.service('browserStorageService', function(localStorageService) {
localStorageService.clearAll();
return localStorageService;
});
app.service('entityService', function(Restangular) {
Restangular.setBaseUrl('http://localhost:8000/rest/');
return Restangular;
});
I'm not at all comfortable with the asynchronous nature of JavaScript, I'm sure it's pretty simple, but I can't get my head around what I can do to correct the situation.
The data is not loader into the page at the fist call made to the controller, but when I call it again without reloading the app, the data is there.
Thanks for your help!
Instead of calling inventoryService.getProduces(); in controller you must create resolve object in config section of application with data from service. After that you can have access to data passed to controller.
app.config(function($routeProvider){
$routeProvider
.when('/',{
template:'',
controller: 'InventoryController',
resolve:{
products: function(inventoryService) {
return inventoryService.getProduces();
}
}
});
});
app.controller('InventoryController', function($scope, products) {
$scope.productList = products;
console.log($scope.productList);
});
Template and route path should be setup according to your application structure.
I've created $http and REST API interface in AnguarJS service as a function that gets injected into different controllers like this:
// Global service to share between states
.service("appSharedService", ['$http', function($http) {
// Method: Returns list of all cities.
this.restCitiesGet = function() {
return $http.get('http://example/nkhorasaniec7/api/v0/city');
};
// Method:
this.citiesGet = function() {
this.restCitiesGet().success(function (data) {
console.log(data);
return data;
})
};
}])
console.log(data); returns the right json output when I call citiesGet() .
// Main controller that prints list of cities.
.controller('CityList', ['$scope', function($scope, appSharedService) {
$scope.cities = appSharedService.citiesGet();
console.log($scope.cities);
}]);
This is my controller injecting my service. console.log($scope.cities); here returns undefined.
$scope.cities value doesn't get changed after route calls this controller.
Is there something wrong with my setup?
Something interesting is that after I change route and come back to this controller again, this time $scope.cities have my REST data and everything's fine.
I think there's something wrong with timing or asynchronous functionality problem here that I'm not aware of.
EDIT:
I could have had $http in my controller and this works all well:
.controller('CityList', ['$scope', '$http', function($scope, $http, appSharedService) {
$http.get('http://localhost/nkhorasaniec7/api/v0/city').success(function (data) {
$scope.cities = data;
});
}]);
But I want to implement helper functions for this.
I would say that the common approach would be to return the promise directly to the controller, much like you have mentioned above by directly using the http request.
// Global service to share between states
.service("appSharedService", ['$http', function($http) {
// Method: Returning the promise
this.citiesGet = function() {
return $http.get('http://example/nkhorasaniec7/api/v0/city');
};
}])
Controller:
.controller('CityList', ['$scope', '$http', function($scope, $http, appSharedService) {
appSharedService.citiesGet().success(function (data) {
$scope.cities = data;
});
}]);
I think you are right about the timing issue. From what I understand, you are getting a promise, that at the moment you do console.log($scope.cities) is not yet resolved.
If you use $scope.cities inside your page, you should see the results as soon as they are loaded. Another option would be to use the promise then function if you really want to log.
$scope.cities = appSharedService.citiesGet().then(function(data) {
console.log(data);
return data;
};
Answering my own question:
I'm trying to make this happen in my a controller defined in my view using ng-controller, not a controller linked to a router (otherwise you could use resolve property like this Delaying AngularJS route change until model loaded to prevent flicker).
And I want to use REST using $http as a factory/service helper function for a cleaner code.
// Global service to share between states
.service("appSharedService", ['$http', '$q', function($http, $q) {
this.citiesGet = function() {
var deferred = $q.defer();
$http({method: 'GET', url: 'http://localhost/nkhorasaniec7/api/v0/city'}).success(function(data) {
deferred.resolve(data);
}).error(function(data, status) {
deferred.reject(data);
});
return deferred.promise;
};
}])
I used angular $q promise here.
// Our main controller that prints list of cities.
.controller('CityList', ['$scope', 'appSharedService', function($scope, appSharedService) {
var promise = appSharedService.citiesGet();
promise.then(
function(data){$scope.cities = data;}
,function(reason){alert('Failed: ' + reason);}
);
}])
And used then function to use that promise.
And now it always updates $scope.cities in any situation that template loads (not just in ng-view)
You can use $q service
.service("appSharedService", ['$http', '$q', function($http, $q) {
// Method: Returns list of all cities.
this.restCitiesGet = function() {
var deffered = $q.defer();
$http.get('http://example/nkhorasaniec7/api/v0/city').then(
//success
function(response){
deffered.resolve(response.data);},
//error
deffered.reject();
);
return deffered
};
and after that you can use promise in you controller
.controller('CityList', ['$scope', function($scope, appSharedService) {
$scope.cities = []
appSharedService.citiesGet().then(
//success
function(result){
angular.copy(result, $scope.cities)
console.log($scope.cities);
},
//error
function(){
console.log("load error");
});
}]);
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 .