I am trying to design an Angular application that has components which register a callback function and a 'data request object' with an Angular service. This service basically keeps track of all the data request objects and which callback functions they refer to. Then, it performs long polling to make asynchronous calls to a RESTful API. When all the data comes in, the service decides which components need which pieces of data and calls into them with the results of the API call.
The problem I am having trouble wrapping my head around is that each component may need the data 'transformed' into a JSON object that prescribes to a specific format. For example, a chart component may require the data result to look one way, but a table component that may required the data result to look another way.
To make things even more complicated, I want to design my data service in such a way that the component can register for data from multiple different RESTful APIs.
Since I'm fairly new to Angular, I wanted to get help on some best practices for accomplishing this type of application. What I am thinking of doing is having a 'primary' data service that my components register with. The registration function will take as arguments a callback function and a data request object that will be in a format like so:
{
"service": "apiService",
"request": {
...
}
}
Then there will be a separate Angular service for each RESTful API. These sub-services will handle what to do with the data request object and correspond to the 'service' field above. The primary data service will then queue up requests to the sub-services on a long polling cycle. So the primary data service will look something like (NOTE: I am using ES6):
class DataService {
constructor($q, $interval, $injector) {
this.$q = $q;
this.$interval = $interval;
this.$injector = $injector;
this.subscriptions = [];
this.callbacks = [];
this.poll();
}
poll() {
let reqs = [];
angular.forEach(this.subscriptions, (sub, i) => {
let service = this.$injector.get(sub.service);
let deferred = this.$q.defer();
reqs.push(deferred);
service.get(sub.request).then((result) => {
this.callbacks[i](result);
deferred.resolve(result);
}, (result) => {
deferred.reject(result);
});
});
this.$q.all(reqs).then(() => {
this.$interval(poll, this.pollInterval || 10000);
});
}
register(request, callback) {
this.subscriptions.push(request);
this.callbacks.push(callback);
}
}
angular.module('DataService', []).service('DataService', DataService);
The piece that I am having trouble figuring out how to implement is the 'data transform' piece. As far as I can tell, there's really only two places I can see where this data transform can take place:
Inside the component
Inside the individual API services
The first way doesn't seem like a viable option to me as it breaks the common practice that components should be somewhat 'dumb.' The component should not handle transforming the data that the RESTful API returns: it should just use the data as-is.
But the second way also poses another problem, which is that each RESTful API service would have to have transform functions for every component type that I've created. This somehow doesn't seem 'clean' to me.
Is there another way I can design my application to achieve this goal? Any insight would be appreciated.
Just a suggestion: use angular's built-in event system.
//Constructor
constructor($q, $interval, $injector, $rootScope) {
this.$q = $q;
this.$interval = $interval;
this.$injector = $injector;
this.$rootScope = $rootScope;
this.listeners = [];
//No idea how its written in Angular2 but cleanup the event listeners when this is destroyed
//Example in 1.5.x:
//$scope.$on('$destroy', () => angular.forEach(this.listeners, l => l());
this.poll();
}
//The new register method
register(ev, callback) {
//When cleaning up - iterate over listeners and invoke each destroy function - see constructor
this.listeners.push(this.$rootScope.$on(ev, callback));
}
//The new poll method
poll() {
let reqs = [];
angular.forEach(this.subscriptions, (sub, i) => {
let service = this.$injector.get(sub.service);
let deferred = service.get(sub.request).then((response) => {
//responseToEventsMapper should map and parse response to list of events to fire.
//Lets say the response is an authentication response for login attempt,
//then the triggered events will be 'Auth:stateChange' and 'Auth:login' with
//response.user as data. configure responseToEventsMapper upfront.
let events = this.responseToEventsMapper(response);
angular.forEach(events, event => this.$rootScope.$emit(event.type, event.data));
});
reqs.push(deferred);
});
this.$q.all(reqs).then(() => {
this.$interval(poll, this.pollInterval || 10000);
});
}
Related
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);
}
}
}]
Using AngularJS I have a historicalDataController
angular.module('armsApp').controller('historicalDataController', ($scope, $historicalFactory) => {
$scope.histogram = new Histogram('#histogram');
historicalFactory.getHistorycalData().then((data) => {
$scope.histogram.update(data);
});
});
and historicalFactory
angular.module('armsApp').factory('historicalFactory', ['$q', '$http',
function ($q, $http) {
return {
getHistoricalData() {
const deferred = $q.defer();
function loadAll(page) {
const token = cognito.getToken('id');
// let deferred = $q.defer();
return $http({
// details
})
.then((response) => {
if (response.data.pages > page) {
loadAll(page + 1);
} else {
deferred.resolve(response.data);
}
});
}
loadAll(0);
return deferred.promise;
},
The former manages my D3 histogram element and the latter handles communication to my data server.
From historicalDataController I am initiating a call to a function in historicalFactory which using recursion does serial GETs to a paginated REST endpoint and aggregates the data. In the controller I currently update the D3 histogram when the function returns with the full dataset, which functions OK.
Now I want to incrementally update the histogram with results from each GET but the callbacks are in the factory which has no access to the UI element (and I feel like view-related logic does not belong in there).
What are some good options of patterns to use here?
From factory should I write the incremental data to a variable in controller and broadcast an event to controller? This feels messy somehow.
Could I use some kind of cross-module decorator to wrap the Histogram-update function with the recursive GET function?
You'll want to use the "notification" callback to provide updates on a promise without resolving it. When one of your $http calls resolves, call deferred.notify(). In your controller, you'll want to provide "catch" and "notifiy" callbacks in your .then().
See the full documentation of $q.
https://docs.angularjs.org/api/ng/service/$q
Currently I have calls like this all over my three controllers:
$scope.getCurrentUser = function () {
$http.post("/Account/CurrentUser", {}, postOptions)
.then(function(data) {
var result = angular.fromJson(data.data);
if (result != null) {
$scope.currentUser = result.id;
}
},
function(data) {
alert("Browser failed to get current user.");
});
};
I see lots of advice to encapsulate the $http calls into an HttpService, or some such, but that it is much better practice to return the promise than return the data. Yet if I return the promise, all but one line in my controller $http call changes, and all the logic of dealing with the response remains in my controllers, e.g:
$scope.getCurrentUser = function() {
RestService.post("/Account/CurrentUser", {}, postOptions)
.then(function(data) {
var result = angular.fromJson(data.data);
if (result != null) {
$scope.currentUser = result.id;
}
},
function(data) {
alert("Browser failed to get current user.");
});
};
I could create a RestService for each server side controller, but that would only end up calling a core service and passing the URL anyway.
There are a few reasons why it is good practice in non-trivial applications.
Using a single generic service and passing in the url and parameters doesn't add so much value as you noticed. Instead you would have one method for each type of fetch that you need to do.
Some benefits of using services:
Re-usability. In a simple app, there might be one data fetch for each controller. But that can soon change. For example, you might have a product list page with getProducts, and a detail page with getProductDetail. But then you want to add a sale page, a category page, or show related products on the detail page. These might all use the original getProducts (with appropriate parameters).
Testing. You want to be able to test the controller, in isolation from an external data source. Baking the data fetch in to the controller doesn't make that easy. With a service, you just mock the service and you can test the controller with stable, known data.
Maintainability. You may decide that with simple services, it's a similar amount of code to just put it all in the controller, even if you're reusing it. What happens if the back-end path changes? Now you need to update it everywhere it's used. What happens if some extra logic is needed to process the data, or you need to get some supplementary data with another call? With a service, you make the change in one place. With it baked in to controllers, you have more work to do.
Code clarity. You want your methods to do clear, specific things. The controller is responsible for the logic around a specific part of the application. Adding in the mechanics of fetching data confuses that. With a simple example the only extra logic you need is to decode the json. That's not bad if your back-end returns exactly the data your controllers need in exactly the right format, but that may not be the case. By splitting the code out, each method can do one thing well. Let the service get data and pass it on to the controller in exactly the right format, then let the controller do it's thing.
A controller carries out presentation logic (it acts as a viewmodel in Angular Model-View-Whatever pattern). Services do business logic (model). It is battle-proven separation of concerns and inherent part of OOP good practices.
Thin controllers and fat services guarantee that app units stay reusable, testable and maintainable.
There's no benefit in replacing $http with RestService if they are the same thing. The proper separation of business and presentation logic is expected to be something like this
$scope.getCurrentUser = function() {
return UserService.getCurrent()
.then(function(user) {
$scope.currentUser = user.id;
})
.catch(function(err) {
alert("Browser failed to get current user.");
throw err;
});
});
It takes care of result conditioning and returns a promise. getCurrentUser passes a promise, so it could be chained if needed (by other controller method or test).
It would make sense to have your service look like this:
app.factory('AccountService', function($http) {
return {
getCurrentUser: function(param1, param2) {
var postOptions = {}; // build the postOptions based on params here
return $http.post("/Account/CurrentUser", {}, postOptions)
.then(function(response) {
// do some common processing here
});
}
};
});
Then calling this method would look this way:
$scope.getCurrentUser = function() {
AccountService.getCurrentUser(param1, param2)
.then(function(currentUser){
// do your stuff here
});
};
Which looks much nicer and lets you avoid the repetition of the backend service url and postOptions variable construction in multiple controllers.
Simple. Write every function as a service so that you can reuse it. As this is an asynchronous call use angular promise to send the data back to controller by wrapping it up within a promise.
I have read many articles but still could not find how to put stuff in services.
Currently this is my Service
angular
.module('users')
.factory('objectiveService', objectiveService);
objectiveService.$inject = ['$http', 'Restangular'];
function objectiveService($http, Restangular) {
return {
getObjectives: getObjectives,
getSingleObjective: getSingleObjective
};
function getObjectives(pid) {
var pr = Restangular
.all('api')
.all('users')
.one('subjects', pid)
.all('objectives');
return pr;
}
function getSingleObjective(oid) {
var pr = Restangular
.all('api')
.all('users')
.one('objectives', oid);
return pr
}
}
This is the controller:
var _vm = this;
this.objPromise = objectiveService.getObjectives(44);
function getData() {
var promise = _vm.objPromise;
promise
.getList(filters)
.then(function(result) {
$scope.gridData = result;
});
}
function remove(id) {
if (confirm('Are you sure you want to delete this!')) {
objectiveService.getSingleObjective(id).remove().then(function() {
$scope.getData();
});
}
}
// initial call
$scope.getData();
In this code i basically see no use to define service because I still has to use then() in the controller to assign data to Grid.
Also I can't use then() in service because there I don't have $scope to update the data.
People say to do all stuff in service but I am not able to figure out how.
Ideally I want to put all functions like remove(object_id) in service
IS it possible that I can just do objectiveService.remove(id) in controller.
But then I have to call $scope.getData() after deleting which I can't do in service
The reason to use services is not to avoid the use of .then. In your controller you would use .then like so:
objectiveService.getObjectives(filters)
.then(function(result) {
$scope.gridData = result;
});
.then is required as the call to get data occurs asynchronously.
Some reasons taken from John Papa's style guide:
The controller's responsibility is for the presentation and gathering of information for the view. It should not care how it gets the data, just that it knows who to ask for it.
This makes it easier to test (mock or real) the data calls when testing a controller that uses a data service.
Data service implementation may have very specific code to handle the data repository. This may include headers, how to talk to the data, or other services such as $http. Separating the logic into a data service encapsulates this logic in a single place hiding the implementation from the outside consumers (perhaps a controller), also making it easier to change the implementation.
If you put the code in the controller it also means it's not reusable. You can't use it in more than one controller or you can't you it in another service.
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"/>