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.
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'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$);
}
}
This subject has been already asked but I couldn't figure out what to do in my case.
Using AngularJS 1.0.5:
Before showing the view "login", I want to get some data and delay the view rendering while the data isn't loaded from an AJAX request.
Here is the main code. Is it the good way?
angular.module('tfc', ['tfc.config', 'tfc.services', 'tfc.controllers']).config([
'$routeProvider', '$locationProvider', '$httpProvider',
function($routeProvider, $locationProvider, $httpProvider) {
$routeProvider.when('/login', {
templateUrl: 'views/login.html',
controller: "RouteController",
resolve: {
data: function(DataResolver) {
return DataResolver();
}
}
});
}
]);
module_services = angular.module("tfc.services", []);
module_services.factory("DataResolver", [
"$route", function($route) {
console.log("init");
return function() {
// Tabletop is a lib to get data from google spreadsheets
// basically this is an ajax request
return Tabletop.init({
key: "xxxxx",
callback: function(data, tabletop) {
console.log("[Debug][DataResolver] Data received!");
return data;
}
});
};
}
]);
The point of AngularJS is that you can load up the templates and everything and then wait for the data to load, it's meant to be asynchronous.
Your view should be using ng-hide, ng-show to check the scope of the controller so that when the data in the scope is updated, the view will display. You can also display a spinner so that the user doesn't feel like the website has crashed.
Answering the question, the way you are loading data explicitly before the view is rendered seems right. Remember that it may not give the best experience as there will be some time to resolve that, maybe giving an impression that your app stopped for some moments.
See an example from John Pappa's blog to load some data before the route is resolved using angular's default router:
// route-config.js
angular
.module('app')
.config(config);
function config($routeProvider) {
$routeProvider
.when('/avengers', {
templateUrl: 'avengers.html',
controller: 'Avengers',
controllerAs: 'vm',
resolve: {
moviesPrepService: function(movieService) {
return movieService.getMovies();
}
}
});
}
// avengers.js
angular
.module('app')
.controller('Avengers', Avengers);
Avengers.$inject = ['moviesPrepService'];
function Avengers(moviesPrepService) {
var vm = this;
vm.movies = moviesPrepService.movies;
}
You basically use the resolve parameters on the route, so that routeProvider waits for all promises to be resolved before instantiating the controller. See the docs for extra info.