Suppose there is a size several link. Every link click is handled by controller. Consider the situation:
User visit some page. Let say that it is /search where user inputs keywords and press search button.
A background process started (waitint for search response in our case)
user goes to another link
after some time user goes back to fisrt page (/search)
At the point 4 angulajs load page as user goes to it at first time. How to make angulajs remeber not state but process? E.g. if process is not finished it shows progress bar, but if it finished it give data from process result and render new page. How to implement that?
Notes
I have found this but this is about just state without process saving.
I have found that but this is about run some process at background without managing results or process state (runing or finished)
You can use angularjs service to remember this "Process" of making an api call and getting the data from it .
here is a simple implementation.
the whole idea here is to create a angular service which will make an api call,
store the data aswell as the state of the data, so that it can be accessed from other modules of angularjs. note that since angularjs services are singleton that means all of their state will be preserved.
app.service('searchService', function() {
this.searchState={
loading: false,
data: null,
error: null
}
this.fetchSearchResults = function(key){
// call api methods to get response
// can be via callbacks or promise.
this.searchState.loading=true;
someMethodThatCallsApi(key)
.then(function(success){
this.searchState.loading=false;
this.searchState.data=success;
this.searchState.error=null
})
.catch(function(error){
this.searchState.loading=false;
this.searchState.data=null;
this.searchState.error=error
});
}
this.getState = function(){
return this.searchState
}
});
// in your controller
app.controller('searchController',function(searchService){
// in your initialization function call the service method.
var searchState = searchService.getState();
// search state has your loading variables. you can easily check
// and procede with the logic.
searchState.loading // will have loading state
searchState.data // will have data
searchState.error // will have error if occured.
});
Even if you navigate from pages. the angular service will preserve the state and you can get the same data from anywhere in the application. you simply have to inject the service and call the getter method.
Based on the question, (a little bit more context or code would help answers be more targeted), when considering async operations within angularJS, its always advisable to use getters and setters within service to avoid multiple REST calls.
Please note - Services are singletons, controller is not.
for eg:
angular.module('app', [])
.controller('ctrlname', ['$scope', 'myService', function($scope, myService){
myService.updateVisitCount();
$scope.valueFromRestCall = myService.myGetterFunctionAssociated(param1, param2);
//Use $scope.valueFromRestCall to your convinience.
}]
.service('myService', ['$http', function($http){
var self = this;
self.numberOfVisits = 0;
self.cachedResponse = null;
self.updateVisitCount = function(){
self.numberOfVisits+=1;
}
self.myGetterFunctionAssociated = function(param1, param2){
if self.cachedResponse === null || self.numberOfVisits === 0 {
return $http.get(url).then(function(response){
self.cachedResponse = response;
return response;
});
}
else {
return self.cachedResponse;
}
}
return {
updateVisitCount: function(){
self.udpateVisitCount();
},
myGetterFunctionAssociated : function(param1, param2){
return self.myGetterFunctionAssociated(param1, param2);
}
}
}]
Related
I have an existing application that uses a MapProvider like so:
mapModule.factory('MapProvider', ['$injector', function($injector) {
return $injector.get('GoogleMapsService');
}]);
This MapProvider is used extensively across the application and is injected into various other controllers and services (rightly or wrongly).
I now need to add a BaiduMapsService, which I have been able to get working as a test with:
mapModule.factory('MapProvider', ['$injector', function($injector) {
if(true) {
return $injector.get('GoogleMapsService');
} else {
return $injector.get('BaiduMapsService');
}
}]);
And flipping the if value accordingly. (Both of these services are using a TypeScript interface, so have the same methods). Now, I need to add a $http call to the API, which will return which map to use, based on the provided data. How can I make my factory asynchronous, without having to change all my MapProvider.someCallHere() calls to MapProvider.then(m => m.someCallHere()).
Ideally, when MapProvider is injected across my application, it will be able to resolve using the async data (only once), and then inject the necessary service afterwards.
Alternatively, is there a way to defer / delay loading Angular at all, until I make an API call and set some global data somewhere?
Thanks.
You can postpone the application bootstrap (also, don't use ng-app, do it manually) until you get data from server. I've answered this before on this question but each case has its own specific details.
I usually see a config value being declared on the app before the application gets bootstraped, this is very useful for multi-tenant apps. So that this preference values can be used in the whole app as an injected provider.
For example:
var app = angular.module('app', []);
// retrieve the $http provider
var ngInjector = angular.injector(["ng"]);
var $http = ngInjector.get("$http");
// load config function. Returns a promise.
function loadConfig(){
return $http.get("/config.json").then(function(response) {
// declare the configuration value on your app
app.constant("Config", response.data);
}, function(err) {
console.error("Error loading the application config.", err)
});
}
// Call loadConfig then bootstrap the app
loadConfig().then(function () {
angular.element(document).ready(function() {
angular.bootstrap(document, ["app"]);
});
});
Finally from your factory, you can use the Config constant to retrieve the preferred map.
mapModule.factory('MapProvider', ['$injector', 'Config', function($injector, Config) {
if(Config.preferedMap == 'GoogleMap') {
return $injector.get('GoogleMapsService');
} else {
return $injector.get('BaiduMapsService');
}
}]);
Only way I can think is to hold initialize whole angular (and modules) until you got your "config" (and set is as global variable).
In my application I have an orchestration service that gets the uris from different services that register with it. service_1 and service_2 could be on different machines but one registered, the uris of their machines will be stored.
In my other application which makes use of that orchestration service, I want to call to the orchestration service to get the uris to use, but then I want to set them as Angular constants, or at least be able to use the uri's values.
So this is the service that's going to be using the 'constant' which is the uri pulled from orchestration service:
(function () {
'use strict';
angular
.module('data.model-view', ['restapi'])
.factory('MVService', MVService);
MVService.$inject = ['$http', '$q', 'exception', 'logger', 'restapi'];
/* #ngInject */
function MVService($http, $q, exception, logger, restapi) {
var HOST = restapi.mvservice.HOST;
var MODULE = restapi.mvservice.MODULE;
...
//below is an example of what will use the above host/module in order to
//get the data needed for this application
function getModels() {
return $http.get(HOST + MODULE + '/model/retrieveAll')
.then(success)
.catch(fail);
function success(response) {
return response.data;
}
function fail(e) {
return exception.catcher('XHR Failed for retrieveAll')(e);
}
}
So then this is restapi module where I'd like the constants to be set, so I can have access to them throughout the application, but I need to get them from the orchestration service first.
(function() {
'use strict';
var data = '';
angular
.module('restapi', [])
.factory('restapi', function($http, exception) {
var HOST = %%ORCSERVICE%%;
var MODULE = '/orchestration/service/rest/data';
return $http.get(HOST + MODULE)
.then(success)
.catch(fail);
function success(response) {
//so at this point I can actually access the data I need
//with a console.debug(response.data);
return response.data;
}
function fail(e) {
return exception.catcher('XHR Failed to reach orc')(e);
}
}).constant('restapi', constant());
function constant() {
//set constants here based on the data pulled from above
//ideally I want the result of this to be like
//{
// mvservice: {
// 'HOST': 'http://xxxxxxxxxx.amazonaws.com'
// 'MODULE': '/rest/service/whatever'
// },
// ... //other service here
//}
}
})();
Like I say in the comment above, I can actually get the data I need (the uris) from the $http.get immediately above. I'd just like then to be able to get the data set as a constant, or at least in a form that I can access it. Because when MVService spins up, it needs the its own uir from the orchestration service in order to be able to make its rest calls. Sorry might be a little confusing, let me know if there is a need for clarification.
Try bootstrapping app after getting necessary data:
var injector = angular.injector(['ng']),
http = injector.get('$http');
http.get(HOST + MODULE).then(function (result) {
app.value('restapi', result.data);
angular.bootstrap(document, [app.name]);
});
There is multiple way :
If you want to use angular.constant you can do it by getting the url by delaying bootstrap of angular until you get your values. Because you can't set constant once the ng-app has loaded. To do this see karaxuna's answer.
Another way is to perform your angular queries in the constructor of your service. Because the routing and call of controllers won't happen until all promise of the service's instantiation phase will be resolved. So you can store the result of your request as fields of your service and then access it without any racing problems on the controllers/directives/angular.run parts of your application.
EDIT: Here is the github repo. And you can test the site here.
On the homepage, just open the browser console and you will notice that WaitOn and data are being run twice. When there is no WaitOn, then the data just runs once.
I have setup my pages by extending RouteController and further extending these controllers. For example:
ProfileController = RouteController.extend({
layoutTemplate: 'UserProfileLayout',
yieldTemplates: {
'navBarMain': {to: 'navBarMain'},
'userNav': {to: 'topUserNav'},
'profileNav': {to: 'sideProfileNav'}
},
// Authentication
onBeforeAction: function() {
if(_.isNull(Meteor.user())){
Router.go(Router.path('login'));
} else {
this.next();
}
}
});
ProfileVerificationsController = ProfileController.extend({
waitOn: function() {
console.log("from controller waitOn");
return Meteor.subscribe('userProfileVerification');
},
data: function() {
// If current user has verified email
console.log("from controller data start");
var verifiedEmail = Meteor.user().emails && Meteor.user().emails[0].verified ? Meteor.user().emails[0].address : '';
var verifiedPhoneNumber = Meteor.user().customVerifications.phoneNumber && Meteor.user().customVerifications.phoneNumber.verified ? Meteor.user().customVerifications.phoneNumber.number : '';
var data = {
verifiedEmail: verifiedEmail,
verifiedPhoneNumber: verifiedPhoneNumber
};
console.log("from controller data end");
return data;
}
});
On observing the console in the client, it seems the hooks are being run 2-3 times. And I also get an error on one of the times because the data is not available. The following is the console on just requesting the page once:
from controller waitOn
profileController.js?966260fd6629d154e38c4d5ad2f98af425311b71:44 from controller data start
debug.js:41 Exception from Tracker recompute function: Cannot read property 'phoneNumber' of undefined
TypeError: Cannot read property 'phoneNumber' of undefined
at ProfileController.extend.data (http://localhost:3000/lib/router/profileController.js?966260fd6629d154e38c4d5ad2f98af425311b71:46:62)
at bindData [as _data] (http://localhost:3000/packages/iron_controller.js?b02790701804563eafedb2e68c602154983ade06:226:50)
at DynamicTemplate.data (http://localhost:3000/packages/iron_dynamic-template.js?d425554c9847e4a80567f8ca55719cd6ae3f2722:219:50)
at http://localhost:3000/packages/iron_dynamic-template.js?d425554c9847e4a80567f8ca55719cd6ae3f2722:252:25
at null.<anonymous> (http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:2445:26)
at http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:1808:16
at Object.Blaze._withCurrentView (http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:2043:12)
at viewAutorun (http://localhost:3000/packages/blaze.js?efa68f65e67544b5a05509804bf97e2c91ce75eb:1807:18)
at Tracker.Computation._compute (http://localhost:3000/packages/tracker.js?517c8fe8ed6408951a30941e64a5383a7174bcfa:296:36)
at Tracker.Computation._recompute (http://localhost:3000/packages/tracker.js?517c8fe8ed6408951a30941e64a5383a7174bcfa:310:14)
from controller data start
from controller data end
from controller waitOn
from controller data start
from controller data end
Have I not used the controllers properly?
Without being able to see the rest of the code that you have defined that uses these route controllers (such as templates or route definitions), I cannot accurately speak to the reason for the data function being called multiple times. I suspect that you may be using the ProfileVerificationsController with multiple routes, in which case the data definition for this controller would be executed multiple times, one for each route that uses the controller. Since the data definition is reactive, as you browse through your application and data changes, this might be resulting in the code defined to be rerun.
As for your controller definitions, I would suggest making a few modifications to make the code more robust and bulletproof. First, the ProfileController definition:
ProfileController = RouteController.extend({
layoutTemplate: 'UserProfileLayout',
yieldRegions: {
'navBarMain': {to: 'navBarMain'},
'userNav': {to: 'topUserNav'},
'profileNav': {to: 'sideProfileNav'}
},
onBeforeAction: function() {
if(!Meteor.user()) {
Router.go(Router.path('login'));
this.redirect('login'); // Could do this as well
this.render('login'); // And possibly this is necessary
} else {
this.next();
}
}
});
Notice the first thing that I changed, yieldTemplates to yieldRegions. This typo would prevent the regions from your templates using this route controller to be properly filled with the desired subtemplates. Second, in the onBeforeAction definition, I would suggest checking not only whether or not the Meteor.user() object is null using Underscore, but also checking for whether or not it is undefined as well. The modification that I made will allow you to check both states of the Meteor.user() object. Finally, not so much a typo correction as an alternative suggestion for directing the user to the login route, you could use the this.redirect() and this.render() functions instead of the Router.go() function. For additional information on all available options that can be defined for a route/route controller, check this out.
Now for the ProfileVerificationsController definition:
ProfileVerificationsController = ProfileController.extend({
waitOn: function() {
return Meteor.subscribe('userProfileVerification');
},
data: function() {
if(this.ready()) {
var verifiedEmail = Meteor.user().emails && Meteor.user().emails[0].verified ? Meteor.user().emails[0].address : '';
var verifiedPhoneNumber = Meteor.user().customVerifications.phoneNumber && Meteor.user().customVerifications.phoneNumber.verified ? Meteor.user().customVerifications.phoneNumber.number : '';
var data = {
verifiedEmail: verifiedEmail,
verifiedPhoneNumber: verifiedPhoneNumber
};
return data;
}
}
});
Notice the one thing that I changed, which is to wrap all of your code defined in the data option for your controller with a if(this.ready()){}. This is critical when using the waitOn option because the waitOn option adds one or more subscription handles to a wait list for the route and the this.ready() check returns true only when all of the handles in the wait list are ready. Making sure to use this check will prevent any cases of data unexpectedly not being loaded yet when you are building up your data context for the route. For additional information on defining subscriptions for your routes/route controllers, check this out.
As a final suggestion, for your onBeforeAction option definition in your ProfileController, I would suggest moving this out into its own global hook like so:
Router.onBeforeAction(function() {
if(!Meteor.user()) {
Router.go(Router.path('login'));
} else {
this.next();
}
});
Defining this check in the global hook ensures that you don't have to worry about adding your ProfileController to all of your routes just to make sure that this check is run for all of them. The check will be run for every route every time that one is accessed. Just a suggestion, though, as you may have reasons for not doing this. I just wanted to suggest it since I make sure to do it for every Meteor app that I develop for additional security.
I have an 'Account' service which acts as a centralized data storage used in multiple controllers and views. This service will hold, besides getter and setter methods all data associated with two types of users, being a logged in user and an anomonous user.
The idea behind this service is that it should be used as a centralized data structure which will keep all associated views and controllers in synch. Eg: If a user logs in whe will know his email and all 'newsletter subscription' fields can be prefilled.
The current setup i use is as below. Note that each Async call on the server will return a promise.
// The Service
angular.module('A').provider('AccountService', [function() {
this.$get = function ($resource) {
var Account = {
getLoggedInAccountAsync : function () {
var Account = $resource(WebApi.Config.apiUrl + 'Account/Get');
return Account.get();
},
getUserData : function () {
return Account.userData;
},
setUserData : function (accountData) {
var merge = angular.extend((Account.currentAccountData || {}), accountData);
Account.currentAccountData = merge;
}
};
return Account;
}
});
// Our main app controller. Get's fired on every page load.
angular.module('A').controller('MainController', ['AccountService', function (AccountService) {
var loggedInAccount = AccountService.getLoggedInAccountAsync();
loggedInAccount.$then(loggedInAccountPromiseResolved);
function loggedInAccountPromiseResolved (response) {
if (response.data.isLoggedIn) {
AccountService.setAccountData(response.data);
}
};
return $scope.MainController= this;
}])
// Our specific controller. For example a newsletter subscription.
angular.module('A').controller('SpecificController', ['AccountService', function (AccountService) {
this.getUserData = AccountService.getUserData;
return $scope.Controller = this;
}])
// Newsletter subscription view
<input id="email" type="email" name="email" ng-model="SpecificController.getUserData().UserName"/>
Using the above code ensures that whenever we use the service to bind our data via Controller.getUserData().property to our view it will stay in synch throughout our entire app.
The code above will also throw if for example the .UserName value is not defined, or if there is no account data at all in case of an anomonous user. One way around this is to 'dump' our 'template' object within our service with null values this will ensure the key/values exist. Another way would be to use watchers and let our controller(s) 'write' to our service in case an user fills a field.
The first option gives me more flexability as i can centralize all the data binding in the service but it feels dirty because you never know what will happen with the server data, it could come in as a different format for example.
Does anyone have any other solution? I would prefer to not let my controllers do the writing of the data to the service.
Thank you for your time!
There are many questions here, I will answer as best as I can.
No, what you are doing is not a "best practice".
First, The model should be injected directly into the view, and maintaining the value is the responsibility of the Controller, with the help of "$scope". Your "ng-model="Controller.getUserData().UserName" is bad. There is a good example of what to do in the end of the blog article I noticed below.
Second, when you fetch data from a service, most of the time, the answer will be asynchronous, so you'd better take a look at the Promise API. The official doc of AngularJS is not always fantastic and sometimes the answer can be found on google groups or in blog article.
For your problem, here is a good article : http://markdalgleish.com/2013/06/using-promises-in-angularjs-views/
Firstly, if you use service in AngularJS, we expect a call to server or whatever that may not return data instantly. So we have two approaches of using ugly callback or beautiful $q.
I prefer using promise since it's cleaner to write. I will rewrite your snippets in a better way :
// Service
angular.module('A').provider('AccountService', [function() {
this.$get = function ($resource, $q) {
var Account = {
getUserData : function () {
var d = $q.defer();
d.resolve(Account.userData);
return d.promise;
}
};
return Account;
}
});
// Controller
angular.module('A').controller('Controller', ['AccountService', function (AccountService) {
var init = function() {
getUserData();
};
var getUserData = function() {
AccountService.getUserData().then(function(data) { $scope.username = data; });
};
init();
//return $scope.Controller = this; // you don't need to do this
}])
HTML:
<input id="email" type="email" name="email" ng-model="username"/>
I have a look up service which connects with the API service to bind the dropdown lists.
var Lookup = angular.module('Lookup', [])
.run(function ($window, $rootScope, DropDownLookUp) {
debugger;
$rootScope.MaritalStatusList = DropDownLookUp.maritalStatusList();
$rootScope.ProvinceList = DropDownLookUp.provinceList();
$rootScope.GenderList = DropDownLookUp.genderList();
$rootScope.ProvinceOfEmploymentList = DropDownLookUp.provinceOfEmploymentList();
});
I am using $Http.Get method to fetch the data.
var maritalStatusList = function () {
var keyName = "dropdown-maritalstatus-list";
// debugger;
var data = StoreData.retrieveStaticData(keyName);
if (data == null) {
HttpService.Get(config.apiUrl + "HomeAPI/MaritalStatusLookUp", "maritalStatusList", "maritalStatusList").then(function (results) {
StoreData.saveStaticData(JSON.stringify(results), keyName);
data = results;
return data;
});
}
else {
return data;
}
};
This look up module is being called when my default App module loads.
Service is getting fired correctly. But my page is getting loaded before the above calls completed. Hence no data displayed in the dropdown.
How do I can delay the page load, until I have all the necessary data?
Easy method - Use $routeProvider resolve method. - Delays navigation to a page until all promises are fulfilled
Other method - Don't render dom elements or secondary apps:
.
The gist of the answer below is to simply wrap any necessary dom elements in an ng-if, and set the evaluated expression to true within the success callback of your $http request. The example below is a bit overkill, in that it's using 2 apps on the page.
This is probably an ugly no-no solution, but it does work. In essence, I'm creating multiple apps on the page (which requires manual bootstrapping). The second app has a dependency on the first, and is rendered within the fake "success callback" of the first app's fake $http via ng-if.
Notice there is no ng-app reference, because the app is manually bootstrapped using the element's id:
<section ng-if="loaded" id="myapp" ng-controller="MyNameIsController">
My Name is {{FirstName}} {{LastName}}! {{loaded}}
</section>
I'm simulating an $http request with a $timeout in the first app:
HelloWorldApp.run(function($rootScope, $timeout){
$timeout(function(){
$rootScope.loaded = true;
},1500)
})
Here's the plunker I forked to get it going.