Managing data models in AngularJS the correct way - angularjs

As I scour the internet, I am finding so many different ways to manage the data model that is used in our Angular templates, but they all only show a small part of the bigger picture. In a large application we need to glue together API data to some form of JavaScript model which in turn is used within our template, but I am not sure how this should all be managed.
After reading this article on AngularJS models I am now aware that I should be wrapping all of my models in a service so the information is available across multiple controllers. One of the only things is that it does not explain how to tie these Services in with API requests.
Here is my current implementation.
Customer Model
var Customer = function (obj) {
var self = this;
if (!obj) {
self.Name = null;
self.Address = null;
self.PrimaryEmailAddress = null;
self.SecondaryEmailAddress = null;
self.PhoneNumber = null;
} else {
self = obj;
}
return self;
}
Then in my controller I use this model on my $scope like
Customer Controller
app.controller('CustomerController', [function($scope, API){
$scope.model = {};
API.Account.getCustomer({id:'12345'}, function(data){
$scope.model = new Customer(data);
});
}]);
This is what my API service looks like
API Service
app.factory("API", ["$resource", function ($resource) {
var service = {};
service.Account = $resource('/WebApi/Account/:type/:id', {},
{
getCustomer: { method: 'GET', params: { type: 'Customer' } }
});
return service;
}])
This has worked fairly well up until now, when I realized that there is API information that is gathered in a parent controller that is now needed in a child controller
Going back to the article linked above, I now can change my models around so they are wrapped in an Angular Service and are therefore available across multiple controllers. This new CustomerService may look like this
NEW CustomerService
app.factory('CustomerService', function() {
var CustomerService = {};
var customer = new Customer();
CustomerService.getCustomer = function() { return customer; }
CustomerService.setCustomer = function(obj) { customer = obj; }
return CustomerService;
});
This isn't quite like the article (much simpler) but it basically contains the OO-like functions to get and set the Customer object.
The problem with this way is we still don't have access to the actual API endpoint to get the customer data from the server? Should we keep the API Service and the Model Services seperate or try to combine them into a single Service? If we do this then we also need a way to differentiate between actually getting fresh data from the server and just getting the currently set Customer object in the singleton object.
I would like to know your initial response to this predicament?
UPDATE
I came across this article that brings together all the functionality of a model including the API requests into a factory service

Model service and API service should be separate. The API service can depend on model service and return model objects instead of directly returning what came from api.
app.factory("API", ["$resource",'Customer','$q' function ($resource,Customer,$q) {
var service = {};
service.Account = function(accountId) {
var defer=$q.defer();
var r=$resource('/WebApi/Account/:type/:id', {},
{
getCustomer: { method: 'GET', params: { type: 'Customer' }}
});
r.getCustomer({id:'12345'}, function(data){
defer.resolve(new Customer(data));
})
return defer.promise;
}
return service;
}]);
This way you have combined the model service and API service. I am assuming Customer is a model service rather than the Customer object.
Update: Based on your follow up question, i thought there could be a better way, but i have not tried it. Rather than creating own promise every time we can use the resource promise:
service.Account = function(accountId) {
var r=$resource('/WebApi/Account/:type/:id', {},
{
getCustomer: { method: 'GET', params: { type: 'Customer' }}
});
return r.getCustomer({id:'12345'}).$promise.then(function(data) {
return new Customer(data);
});
}
Since the then method itself returns promise that is resolved to return value of the then callback statement this should work.
Try this approach and share your findings

Related

Same angular service injection into factory and Controller

Currently for loading a minimum of 100 customer booking records, We have used Angular factory as InitializationFactory for having the data in hand before the page loads and this factory is registered in Routes. This factory makes use of customerService to talk to relevant API. This whole request takes round about 5-7seconds to complete. We have the requirement of applying filters on top of the fetched data.
Considering page performance in mind, We now want to fetch only 10 records with dynamic filters applied over it. Next set of records would be retrieved based on a click event. Now, in order to do so, I need to inject customerService into CustomerController as well.
So, by design, is it a good approach to inject customerService into factory and then into controller for further data retrieval?
Code Snippet:
Route.js
.state("app.customer.dashboard",
{
parent: "parent",
controller: "CustomerBookingController",
resolve: {
customerBookingListModel: [
"CustomerInitializationFactory",
function (customerInitializationFactory) {
return customerInitializationFactory.initializeCustomerBookingResult();
}
]
}
CustomerInitializationFactory.js
function CustomerInitializationFactory(customerService, $translate, bookingConstants) {
return {
initializeCustomerAccountResult: function() {
var params = "xyz";
var promise = customerService.getCustomerBookings(params)
.then(function(bookingResponse) {
var customerBookings = constructCustomerBookings(bookingResponse);
return customerBookings;
});
return promise;
} };
CustomerBookingController.js
function CustomerBookingController(customerBookingListModel,customerService???) {
$scope.LoadMoreBookings = function(params) {
var bookings = customerService.getCustomerBookings(params);
};

AngularJS - Handling API Data

Currently developing an app which requires pulling in data using an API, and running into the following problem.
In my controller I pull in a list of products from an API, add them to $rootScope, than loop over in order to display a list of all products.
When viewing an individual product, I loop over the list and display the product with the requested ID into a view like so
getProduct: function(productID) {
var products = $rootScope.products;
for (var i =0; i<products.length; i++) {
if (products[i].id == parseInt(productID)) {
return products[i];
}
}
}
This all works fine, except for if you visit the individual product URL without first going through the main list page, the list of products is unavailable as the call to the API is never made.
What is the best way to about resolving this?
Creating another separate API call from this view seems very messy and overly complicated and I was wondering if there is a standard or better approach!
I would create an angular service for product that has a private array for products with a getter & setter. This will allow you to stay away from using $rootScope to pass data between controllers (which is a good thing).
Then have a function that makes a call to the API for each product detail page. You can then pass the ID to the page in the URL using $routeParams
By putting this all in a service it becomes reusable to all your controllers and quite clean.
var myModule = angular.module('myModule', []);
myModule.service('productService', function() {
var products = [1,2,3,4,5];
this.addProduct = function(id){
products.push(id);
}
this.getProducts = function(){
return products;
}
this.getProductDetail = function(id){
//$http.get(...
}
});
Edit: example implementation as requested
// Don't forget to inject your service into your controller
myModule.controller('myCtrl', ['productService', function (productService) {
// Add a product to your array
productService.addProduct(6); //products = [1,2,3,4,5,6]
// Retrieve products
var myProducts = productService.getProducts(); //myProducts = [1,2,3,4,5,6]
// You can do this a few different ways but assuming
// getProductDetail returns a promise from its $http call
productService.getProductDetail(4).then(function (productDetail) {
$scope.productDetails = productDetail;
});
}]);
Came up with a solution based on your answer (removing the dependency on $rootScope really helped) so marking your answer as good to go but wanted to share what I came up with in case anyone finds it useful!
Essentially, in the product detail controller which deals with displaying an individual product we say, if we already have the product information within the application, pull it from that, and, if we don't then make a call to the API.
I have an extra service called config which I need to access the my API in case anyone is wondering whats going on there :)
Service
productsApp.factory('$productService', function($q,$http,$rootScope,$cookies,$state,$configService) {
var products = '';
return {
//Get all products
getProducts: function() {
//$http.get....
//Dont forget to use $q.defer() for your promise
},
returnProducts: function() {
return products;
},
//Get specific product
getProduct: function(productID) {
for (var i =0; i<products.length; i++) {
if (products[i].id == parseInt(productID)) {
return products[i];
}
}
}
}
});
Controller
productsApp.controller('productDetailCtrl', function($scope,$rootScope,$productService,$stateParams,$configService,$cookies) {
var products = $productService.returnProducts();
if (products == '') {
$configService.getToken().then(function(response){
$productService.getProducts().then(function(response){
$scope.product=$productService.getProduct($stateParams.productID);
}, function(error){
console.log(error);
});
}, function(error){
console.log(error);
});
} else {
$scope.product = $productService.getProduct($stateParams.jobID);
};
});

Custom caching for $resource

I have the following pattern in my AngularJS which calls for refactoring:
$scope.advertisers = Advertiser.query()
$scope.advertisersMap = {};
$scope.advertiser.$then(function (response) {
arrayToMap(response.resource, $scope.advertisersMap)
}
arrayToMap is function that adds each item in an array to the object with it's ID as key.
Now, I would have liked that this will happen in Advertiser itself, e.g.
$scope.allAdvertisers = Advertiser.query()
// Somewhere else
var advertiser = Advertiser.get({id: 2})
Where Advertiser.get({id: 2}) will return from a cache populated earlier by the query method.
Advertiser is defined in factory:
.factory('Advertiser', ['djResource', 'Group', function ($djResource, Group) {
var Advertiser = $djResource('/campaigns/advertisers/:advertiserId/', {advertiserId: '#id'});
Advertiser.prototype.getGroups = function () {
return Group.getByAdvertiser({advertiserId: this.id});
};
return Advertiser;
}])
Sadly, DjangoRestResource (and $resource which it wraps) caches by URLs, so query() will cache /advertisers/ while get(2) will cache /advertisers/2/, but I want query to cache in such way that get will be able to retrieve it as well.
I've tried replacing the query function by a wrapper function that does the caching, but I want it to return an promise which is also an Array like $resource does. It was something like:
var oldQuery = Advertiser.query;
var cache = $cacheFactory('advertisers');
Advertiser.query = function () {
var promise = oldQuery();
return promise.then(function (response) {
angular.forEach(response.resource, function (resource) {
cache.put(resource.id, resource)
})
})
};
But then the returned promise is no longer an Array-like object, and it doesn't not encapsulate the returned results in an Advertiser object as it used to, which breaks most of my code expects that Advertiser.query() will eventually be an Array.
What other approaches should I try? This snippet repeats itself in every controller for multiple factories and it hurts my eyes.
Here is solution to the problem:
function makeCachingMethod(object, method, cache, config) {
var $q = angular.injector(['services']).get('$q');
var oldMethod = object[method];
object[method] = function () {
var result;
if (!config.isArray) {
var id = config.idParam ? arguments[0][config.idParam] : arguments[0];
result = cache.get(id);
if (result !== undefined) {
if (result.$promise === undefined) {
result.$promise = $q.when(result);
result.$resolved = true;
}
return result;
}
}
result = oldMethod.apply(this, arguments);
result.$promise.then(function (data) {
if (config.isArray) {
angular.forEach(data, function (item) {
cache.put(item.id, item);
})
} else {
cache.put(data.id, data);
}
return data;
});
return result;
}
}
And an example usage:
app.factory('Country', ['$resource', '$cacheFactory', function ($resource, $cacheFactory) {
var Country = $resource('/campaigns/countries/:id', {id: "#id"}, {
query: {method: 'GET', url: '/campaigns/countries/', isArray: true},
get: {method: 'GET', url: '/campaigns/countries/:id/', isArray: false}
});
var cache = $cacheFactory('countries');
makeCachingMethod(Country, 'query', cache, {isArray: true});
makeCachingMethod(Country, 'get', cache, {idParam: 'id'});
return Country;
}])
What is happening here?
I use makeCachingMethod to decorate the original method created by $resource. Following the pattern used by $resource itself, I use a configuration object to signal whether the decorated method returns an array or not, on how the id is passed in queries. I assume, though, that the key of the ID to save is 'id', which is correct for my models but might need to be changed.
Noticed that before returning an object from the cache, the decorator adds to it $promise and $resolved attributes, since my application expects objects originated from $resource which have these properties, and in order to keep using the promises API, e.g.:
$scope.advertiser = Advertiser.get({advertiserId: $scope.advertiserId});
$scope.advertiser.$promise.then(function () {
$scope.doSomething();
});
Notice that since the function is defined outside the scope of any Angular module it is required to inject the $q service using angular.injector(). A nicer solution will be to return a service for invoking the decorator function. Such service could also handle the generation of the caches themselves.
This solution does not handle the expiration of cached models, which isn't much of problem in my scenario, as these rarely change.
I'm not aware of any cache support directly with $resource, you could create a factory that abstracts the Advertiser resource and handle the caching yourself.
Here is some helpful info on caching:
https://developers.google.com/web/fundamentals/performance/optimizing-content-efficiency/http-caching#validating-cached-responses-with-etags
I'm working on an app at the minute using $resource and every method is abstracting so I can check the cache to see if what I'm requesting is there and return it, if not, make the call using $resource.

What's the proper way to create a data service in AngularJS using $resource?

I'm using Angular's $resource service to make calls to a RESTful web service. Currently I'm doing this:
app.factory('data', function($resource) {
// Creating resource objects for all resources I have.
var usersResource = $resource('http://example.com/api/users/:id', {id: '#id'});
var universitiesResource = $resource('http://example.com/api/universities/:id', {id: '#id'});
// ..
// Initializing more resource objects.
return {
users: {
getAll: function() {
return usersResource.query();
}
// ..
// More methods..
},
universities: {
getAll: function() {
return universitiesResource.query();
}
// ..
// More methods..
}
// ..
// More objects.
}
});
The other way is to have a different data factory for every resource. So, my question is what's the proper way of doing this and why? It looks more structured to have just a single data factory, but are all these resource objects making a lot of burden and reducing the performance of the app?
Your way looks ok if your application not so big and you will not have dozens of resources collected in one place. If so this "global data factory" will be too big and everyone in your team will edit it constantly.
One more consideration - your resources usually more complex then just plain CRUD operations. You have to extend them with additional functions like described in the official docs:
var CreditCard = $resource('/user/:userId/card/:cardId', {userId:123, cardId:'#id'}, {
charge: {
method:'POST',
params:{charge:true}
}
});
Imagine how it will look like if you will put every declaration like this in one place.
I came up to the different approach - to make resources available on demand and even not declare them in case of default CRUD operations.
We have the following factory:
angular.module("app").factory("resourcesByConvention", ResourcesByConvention);
ResourcesByConvention.$inject = ['$injector', '$resource'];
function ResourcesByConvention($injector, $resource) {
return {
getResource: function (resourceName) {
var resource;
try{
resource=$injector.get(resourceName);
}
catch(error)
{
resource = $resource('/api/' + resourceName, {}, {
update: {
method: 'PUT'
}
});
}
return resource;
}
}
}
In every place we injecting this factory and doing:
var usersResource=resourcesByConvention.getResource("users");
If you need only simple CRUD operations for your resource - this factory will just create it for you. But if you need some more complex, you just create and register your own. For example, you will register the following:
app.factory('AccountResource', ['$resource', function($resource) {
return $resource('/account/:id', null,
{
update: { method:'PUT' },
getBalance: {method: 'GET'},
charge: {method: 'POST'}
});
}]);
and request it later:
var accountResource=resourcesByConvention.getResource("AccountResource");
Conclusion: you have single service to get any resource needed and you may not declare typical CRUD resources at all.

Share the same data between few controllers using ngResource

I am new to angular so it is probably easy question. I have this factory resource at the moment:
angular.module('resources.survey', ['ngResource'])
.factory('Survey', function ($resource) {
return $resource('/backend/surveys/:surveyId/data', {surveyId: '#id'});
});
Controller:
.controller('PagesCtrl', function (Survey) {
var survey = Survey.get({surveyId: 2});
//now I want to change survey variable and share it between two controllers
});
There are no problems with ngResource I can get the data from server. However I want to manipulate with the data from the server and use the same data in other controllers (probably using DI) and allow data manipulation there as well. I know that it can be done with $rootScope, but I was wondering if there is any other way.
Your service should cache the response for the resource request in something like array of surveys and dispense surveys from this array instead of directly returning a resource object.
Controllers would only share data if the same reference for the survey is returned.
Roughly it would look like
.factory('Survey', function ($resource,$q) {
var surveys[];
return {
getSurvey:function(id) {
var defer=$q.defer();
//if survery contains the survey with id do //defer.resolve(survey[i]);
// else query using resource. On getting the survey add it to surveys result and resolve to the newly added survey.
}
}
});
angular.module('resources.survey', ['ngResource'])
.factory('Survey', function ($resource) {
return $resource('/backend/surveys/:surveyId/data', {surveyId: '#id'});
})
.controller('MyCtrl', function($scope,Survey){
//here you can call all the $resource stuff
});
Here is complete documentation and example how to use it:
http://docs.angularjs.org/api/ngResource.$resource
I managed to create a resource that can handle what I wanted. It is probably not as advanced as Chandermani suggested. But it works for my needs.
angular.module('resources.survey', ['ngResource'])
.factory('Survey', function ($resource) {
var resource = $resource('/backend/surveys/:surveyId/data',
{surveyId: '#id'}
);
var Survey = {};
var data = []; //saves the data from server
Survey.get = function(surveyId) {
if(angular.isDefined(data[surveyId])){
return data[surveyId];
}
return data[surveyId] = resource.get({surveyId: surveyId});
};
return Survey;
});
And to call basically I call it like this:
.controller('QuestionsCtrl', function (Survey) {
Survey.get(1).newData = 'newData'; //adding additional data
console.log(Survey.get(1));
});
I guess this can be improved.

Resources