ANgularJS sync server data with local data on request - angularjs

There is one thing I can't understand in Angular - how better to sync API data with local data.
My structure object is very big, I just give you simple example.
I have 4 api links.
api_links: {
allCars: '/cars/',
cabrio: '/cars/cabrio',
sedan: '/cars/sedan',
mercedes: 'cars/sedan/mercedes'
}
And this is my local object to keep data locally
$rootScope.apiData = {
cars{
cabrio:{},
sedan: {}
}
}
In my single page app I want to reduce count of requests.
So on common page I'm recieving all Cars and put data to $rootScope.apiData
When I go to sedans page, I check if there any data in $rootScope.apiData.cars.sedan. If it's exist, I don't send request.
But if I start from sedans page, I recieve only sedans. Then when I go to common page - HOW can I check am I need to load more categories.
It's quick abstract example. But I try to find any good solutions or plugins for this and cannot.
Please help me!
UPDATE:
Here is my factory:
var api = angular.module('app.api', []);
api.factory('GetData', ['$http', '$q', '$rootScope', '$location'
function($http, $q, $rootScope, $location) {
var apiUrl = '/api/js/';
var apiMixes = {
dashBoard: 'api/dashboard',
accounts: 'api/accounts',
top: 'top',
logout: 'client/logout',
...
}
var methods = {};
methods.getApi = function(u, params) {
var url = apiMixes[u] + '?' + $.param( params );
var deferred = $q.defer();
// Here I need to check if I have cached data
cachedData(data) = ___TODO___;
if( cachedData(data) ){
deferred.resolve(cachedData(data));
// Turn watching on
$rootScope.$emit("receivedApiUpdate", cachedData(data));
return deferred.promise;
}
// If no - get gata from server and put to cache
$http.get(url, {cache: true}).success(function (res, status, headers, config) {
if(res && res.data){
// Turn watching on
$rootScope.$emit("receivedApiUpdate", res.data);
//LocalData.put(res.data);
deferred.resolve(res.data);
}else{
deferred.reject(status);
}
}).error(function (res, status, headers, config) {
(status==401) && $location.path('login/lock');
deferred.reject(status);
});
return deferred.promise;
};
methods.sendData = function(u, data, o) {
data = data || {};
o = o || {};
var deferred = $q.defer();
var url = '/api/js/'+ apiMixes[u];
$http.post(url, JSON.stringify(data)).success(function(res, status) {
if(res.success && res.data){
// Here I need to update my cache with some new value
o.localUpdate && ___PUT_TO_CACHE_TODO___;
deferred.resolve(res.data);
}
});
return deferred.promise;
};
return methods;
}]);
Here is my Controller
app.controller('MyAccountsCtrl', ["$rootScope", "$scope",
function ($rootScope, $scope) {
// Watcher for api updates
$rootScope.$on('receivedApiUpdate',function(event, response){
if(!response || !response.accounts) return;
renderData(response.accounts);
});
function renderData(accounts){
// This renders to template
$scope.accountList = accounts;
}
}]);
Here is mainCtrl, whick make common request, after it I need to update data in several templates.
app.controller('AppCtrl', ['$rootScope', '$scope', 'GetData',
function ($rootScope, $scope, GetData) {
// Here I fire different request for each page, I keep requests in router.
GetData.getApi( 'accounts' ).then(function(data){
// Show content when page is loaded
$('.main-content').removeClass('hidden');
},function(res){
log('GET DATA FAIL', res);
});
}]);

You need to create a service/factory for this, not use the $rootScope. Also, unless you need the data to be constantly updated, you can use the cache: true option inside your $http call.
angular
.module('app')
.factory('carFactory', carFactory);
function carFactory() {
var factory = {
getData: getData
}
return factory;
function getData(callback) {
$http({
method: 'GET',
url: '/cars',
cache: true
}).success(callback);
}
}
And then inside your controller/directives just inject the carFactory and use carFactory.getData(function(cars) { ... }) when you need the data. If the data doesn't exist, it will $http call for it and afterwards execute the callback function. If it does exist, it will return the data directly to the callback, without an $http call.

Related

Angular JS saving information from factory AJAX request

I have a factory where I'm getting data from a server using the $http methods:
.factory('$factory', function ($q, $http, $timeout, $state, $ionicHistory, $localstorage) {
var obj = [];
var functions = {
getData: function () {
var dfd = $q.defer();
if(!obj){
$http({
url: remoteUrl+'/getdata',
method: 'POST',
data: {}
}).then(function(response) {
$timeout(function(){
obj = response.data;
dfd.resolve(response.data);
}, 2000)
}, function(response) {
}
}else{
return obj;
}
return dfd.promise;
}
}
}
So this gets the data and puts it in an object. Correct me if I'm wrong, but this method of using a factory for this type of action is so it's not tied to a controller, and can be used anywhere in my application.
With this in mind, I wish to make it so I can get the data anywhere in my app without having to query the server each time. i.e. once it's queried the server the factory saves the response to an object (like I'm doing now). But I'm having trouble accessing the data afterwards in another controller for example.
I've started to make what I think it should look like by using the if(!obj) line, but in the else statement I cant seem to just return the obj object. It throws errors as it's not returning a promise like it's expected too.
I'm not sure if I'm even along the right lines with this.
Thanks
You're returning a promise so you need to always return a promise even on the cached response, you can do it relatively easy at this level by wrapping it in a $q.when (https://docs.angularjs.org/api/ng/service/$q) which will return a promise immediately resolved.
return $q.when(obj);
Although $http service has built in caching, so you may want to take a look under the Cache section.
https://docs.angularjs.org/api/ng/service/$http
https://www.ng-book.com/p/Caching/
This should work:
Insteada assigning obj = [] assign as a null. Usually I prefer callback. You can try this code:
.factory('$factory', function($q, $http, $timeout, $state, $ionicHistory, $localstorage) {
var obj = [];
var functions = {
getData: function(cb) {
// instead of checking !obj you have to check for length or you have to set obj as null
if (obj && obj.length == 0) {
$http({
url: remoteUrl + '/getdata',
method: 'POST',
data: {}
}).then(function(response) {
obj = response.data;
cb(response.data)
}, function(response) {
}
}
else {
cb(obj)
}
}
}
}
})
// You can use callback by following code
$factory.getData(function(response){
// response will come here
})

Best way to pre-load data from using $http in a service in AngularJs

I am trying to create a service which first loads some data by making an AJAX call using $http.
I am looking at something like:
app.factory('entityFactory', function() {
var service = {};
var entities = {};
// Load the entities using $http
service.getEntityById(entityId)
{
return entities[entityId];
}
return service;
});
app.controller('EntityController', ['$scope', '$routeParams', 'entityFactory', function($scope, $routeParams, entityFactory) {
$scope.entity = entityFactory.getEntityById($routeParams['entityId']);
}]);
I want to make sure that the entities is loaded fully before I return the entity using getEntityById.
Please let me know what would be the right way to do this? One way I know would be to make a synchronous AJAX call, but is there anything better? Can promises be used in this case in a better way?
Tried using $q to check if service is initialized. Clean enough for me, any other methods are welcome :).
app.factory('entityFactory', function($q, $http) {
var service = {};
var _entities = {};
var _initialized = $q.defer();
$http({method: 'GET', url: '/getData'})
.success(function(data, status, headers, config) {
if (data.success)
{
_entities = data.entities;
}
_initialized.resolve(true);
})
.error(function(data, status, headers, config) {
_initialized.reject('Unexpected error occurred :(.');
});
service.getEntityById(entityId)
{
return entities[entityId];
}
service.initialized = _initialized.promise;
return service;
});
app.controller('EntityController', ['$scope', '$routeParams', 'entityFactory', function($scope, $routeParams, entityFactory) {
entityFactory.initialized.then(function() {
$scope.entity = entityFactory.getEntityById($routeParams['entityId']);
});
}]);
You can utilize callbacks within factories to store the data on the first call and then receive the data from the service on every subsequent call:
app.factory('entityFactory', function() {
var service = {};
var entities = null;
// Load the entities using $http
service.getEntityById(entityId, callback)
{
if (entities == null) {
$http(options).success(function(data) {
entities = data;
callback(data);
});
} else {
callback(entities);
}
}
return service;
});
And then you can use this:
entityFactory.getEntityById(id, function(entities) {
//console.log(entities);
});
Passing in a callback or calling $q.defer(), are often signs that you're not taking advantage of promise chaining. I think a reasonable way to do what you're asking is as follows.
app.factory('entityFactory', function($http) {
var service = {};
var _entitiesPromise = $http({method: 'GET', url: '/getData'});
service.getEntityById = function(entityId) {
return _entitiesPromise.then(function(results) {
return results.data.entities[entityId];
});
};
return service;
});
app.controller('EntityController', ['$scope', '$routeParams', 'entityFactory', function($scope, $routeParams, entityFactory) {
entityFactory.getEntityById($routeParams['entityId']).then(function(entity) {
$scope.entity = entity;
}, function() {
// Can still do something in case the original $http call failed
});
}]);
where you only cache the promise returned from $http.

Where in angular should I put this code?

I have an http-method that gets some data from a google spreadsheet. I want to add this to the $scope so I can output it in the DOM. Later I might make a timed loop of this so that the $scope get's updated every 5 seconds or so.
I currently run the code in app.run:
angular.module('spreadsheet2angular', []).
run(function($http){
$http({method: 'GET', url: 'http://cors.io/spreadsheets.google.com/feeds/cells/0Aq_23rNPzvODdFlBOFRYWlQwUFBtcXlGamhQeU9Canc/od6/public/values?alt=json'}).
success(function(data, status, headers, config) {
var entries = data.feed.entry;
var phraces = [];
entries.forEach(function(entry){
var cell = entry.gs$cell;
if(!phraces[cell.row]){
phraces[cell.row] = {};
}
if(cell.col == 1)
{
phraces[cell.row].name = cell.$t;
}
else if(cell.col == 2)
{
phraces[cell.row].value = cell.$t;
}
});
phraces.forEach(function(phrace){
console.log(phrace);
});
}).
error(function(data, status, headers, config) {
console.log('error');
});
});
I'm new to angular, is this the best place to run it? I would like to run it as something that is easily reusable in different projects.
I think from what you've explained, a service would be perfect. Build it out then inject it in your controller. You can then call/use that service object whenever you would like.
I would use service/factory that returns promise. So we call async service method, get back promise and parse response into controller.
If you think to use the same call in the future, you can write generic method.
By the same way, if you are going to parse response by the same way in the future, the part of logic I would put into the service as well and wrap with $q . So the response still will be promise.
And this is an example I use that might help you to understand what I'm meaning:
app.service('apiService', ['$http', '$q', '$rootScope',
function($http, $q, $rootScope) {
var request = function(method, data) {
var deferred = $q.defer();
var configHttp = {
method: 'POST',
url: config.api + '/' + method
};
if (data !== undefined) {
configHttp.data = data;
}
$http(configHttp).success(function(data, status, headers) {
if (data.error === undefined) {
deferred.resolve(data);
} else {
deferred.reject(data);
}
}).error(function(data, status, headers) {
deferred.reject(data);
});
return deferred.promise;
}
return {
getItem: function() {
return request('get_item');
},
getItemByParams: function(id) {
return request('get_item_by_params', {id: id});
}
};
}
]);

Prevent AngularJS to return promise with cached data

I'm using a service in order to share data between controllers. However, the service is returning a promise with cached data even when making new requests. Depending on where the defer instance is created either live data is returned but two-way binding breaks or the two-way binding works but cached data is returned.
How can one prevent the return of a promise with cached data and keep two-way binding?
I've put up a plunker to illustrate the case: http://plnkr.co/edit/SyBvUu?p=preview and for sake of completeness, here is the troublemaking service:
app.service('myService', function($http, $q) {
// When instancing deferred here two way binding works but cached data is returned
var deferred = $q.defer();
this.get = function(userId) {
// When instancing deferred here two way binding breaks but live data is returned
//var deferred = $q.defer();
console.log('Fetch data again using id ', userId);
var url = userId + '.json';
$http.get(url, {timeout: 30000, cache: false})
.success(function(data, status, headers, config) {
deferred.resolve(data, status, headers, config);
})
.error(function(data, status, headers, config) {
deferred.reject(data, status, headers, config);
});
return deferred.promise;
};
});
UPDATE: The problem wasn't that data was cached, it was that I hadn't understood how a data was to be shared and that the shared data can not be a primitive. See my own answer below.
Since $http returns a deferred object what you are doing here is actually overkill. When I changed your service to the following it seems to work fine.
Plunker
app.service('myService', function($http, $q) {
this.get = function(userId) {
console.log('Fetch data again using id ', userId);
var url = userId + '.json';
return $http.get(url, {timeout: 30000, cache: false});
};
});
Edit
To get your controller SecondCtrl to update, the easiest thing to do, while keeping the structure of your code the same, is to broadcast the new data in an event defined in FirstCtrl using $rootScope.$broadcast and capture the broadcasted event in your other controller using $scope.$on. I've updated the Plunker and now your data is in sync.
Modified loadUserFromMyService function in FirstCtrl:
$scope.loadUserFromMyService = function(userId) {
var promise = myService.get(userId);
promise.then(
function(data) {
console.log('UserData', data);
$scope.data = data;
$rootScope.$broadcast('newData', data);
},
function(reason) { console.log('Error: ' + reason); }
);
};
Added in SecondCtrl:
$scope.$on('newData', function (evt, args) {
console.log('dataChanged', args);
$scope.data = args;
});
I came up with simplified solution to share data with the help of Luke Kende. Here is a plunk: http://plnkr.co/edit/JPg1XE?p=preview. See code below.
One important thing is that the shared object isn't a primitive. When I tried different solutions I started with declaring the shared object and assign it null, which is a no-no. Using an empty object makes it work.
var app = angular.module('plunker', []);
// Service
app.service('myService', function($http, $q) {
//object that will be shared between controllers
var serviceData = {
items: []
};
return {
data: serviceData, //pass through reference to object - do not use primitives else data won't update
get: function(url, overwrite) {
if (serviceData.items.length === 0 || overwrite){
$http.get(url, {timeout: 30000})
.success(function(data, status, headers, config) {
//could extend instead of ovewritting
serviceData.items = data;
})
.error(function(data, status, headers, config) {
serviceData.items = {status: status};
});
}
return serviceData;
},
empty: function(){
serviceData.items = [];
},
more: function(){
//do some other operations on the data
}
};
});
// Controller 1
app.controller('FirstCtrl', function( myService,$scope) {
//myService.data is not initialized from server yet
//this way don't have to always use .then() statements
$scope.data = myService.data;
$scope.getTest = function(id){
myService.get('test' + id + '.json',true);
};
$scope.addItem = function() {
$scope.data.items.push({'title': 'Test ' + $scope.data.items.length});
};
$scope.delItem = function() {
$scope.data.items.splice(0,1);
};
});
// Controller 2
app.controller('SecondCtrl', function( myService,$scope) {
//just attach myService.data and go
//calling myService.get() results in same thing
$scope.data = myService.data;
//update the the data from second url -
$scope.getTest = function(id){
myService.get('test' + id + '.json',true);
};
$scope.empty = function(){
myService.empty();
};
});

Share async data between controllers without making multiple requests

I'm trying to make a single $http request to get one of my JSON files and use the data across all my controllers.
I saw on egghead.io how to share data across multiple controllers, and I've also read this StackOverflow question: "Sharing a variable between controllers in angular.js".
However, the answers there don't use the $http module. When using $http, the controllers don't have the data to work on, and by the time the response is received it's already too late.
I then found the method $q.defer and this question on StackOverflow: "AngularJS share asynchronous service data between controllers"
The solution posted there works fine, BUT it has two issues:
Each controller will trigger the $http request to obtain the same data already used in another controller; and,
If I try to manipulate the data received I have a then function.
Below you can see my code:
controllers.js
'use strict';
/* Controllers */
function appInstallerListCtrl($scope, Data) {
$scope.apps = Data;
}
function appInstallerDetailCtrl($scope, $routeParams, Data) {
$scope.appId = $routeParams.appId;
$scope.apps = Data;
console.log($scope.apps); // <-- then function
console.log(Data); // <-- then function with $vv data returned but I can't access it
for (var i in $scope.apps) // <--- no way, baby!
console.log(i);
}
app.js
var app = angular.module('appInstaller', []);
app.factory('Data', function($http, $q) {
var defer = $q.defer();
$http.get('apps.json').then(function(result) {
defer.resolve(result.data.versions.version);
});
return defer.promise;
});
app.config(['$routeProvider', function($routeProvider) {
$routeProvider.
when('/app', {templateUrl: 'partials/app-list.html', controller: appInstallerListCtrl}).
when('/app/:appId', {templateUrl: 'partials/app-detail.html', controller: appInstallerDetailCtrl}).
otherwise({redirectTo: '/app'});
}]);
What I'd like to have is that when launching the app, the $http request will be performed and the response will be used throughout the app across all controllers.
Thanks
I like to store my data in the service, and return a promise to the controllers, because usually you need to deal with any errors there.
app.factory('Data', function($http, $q) {
var data = [],
lastRequestFailed = true,
promise;
return {
getApps: function() {
if(!promise || lastRequestFailed) {
// $http returns a promise, so we don't need to create one with $q
promise = $http.get('apps.json')
.then(function(res) {
lastRequestFailed = false;
data = res.data;
return data;
}, function(res) {
return $q.reject(res);
});
}
return promise;
}
}
});
.controller('appInstallerListCtrl', ['$scope','Data',
function($scope, Data) {
Data.getApps()
.then(function(data) {
$scope.data = data;
}, function(res) {
if(res.status === 500) {
// server error, alert user somehow
} else {
// probably deal with these errors differently
}
});
}]);
Any callbacks that are registered after a promise has been resolved/rejected will be resolved/rejected immediately with the same result/failure_reason. Once resolved/rejected, a promise can't change (its state). So the first controller to call getApps() will create the promise. Any other controllers that call getApps() will immediately get the promise returned instead.
Since you are using a promise, to access the data returned by promise use the callback syntax
function appInstallerDetailCtrl($scope, $routeParams, Data) {
$scope.appId = $routeParams.appId;
Data.then(function(returnedData) {
$scope.apps=returnedData;
console.log($scope.apps);
for (var i in $scope.apps)
console.log(i)
});
}
Make sure this
defer.resolve(result.data.versions.version);
resolve returns array, for the above code to work. Or else see what is there in data and ajust the controller code.
I found the way not sure weather it is a best approach to do it or not.
In HTML
<body ng-app="myApp">
<div ng-controller="ctrl">{{user.title}}</div>
<hr>
<div ng-controller="ctrl2">{{user.title}}</div>
</body>
In Javascript
var app = angular.module('myApp', []);
app.controller('ctrl', function($scope, $http, userService) {
userService.getUser().then(function(user) {
$scope.user = user;
});
});
app.controller('ctrl2', function($scope, $http, userService) {
userService.getUser().then(function(user) {
$scope.user = user;
});
});
app.factory('userService', function($http, $q) {
var promise;
var deferred = $q.defer();
return {
getUser: function() {
if(!promise){
promise = $http({
method: "GET",
url: "https://jsonplaceholder.typicode.com/posts/1"
}).success(function(res) {
data = res.data;
deferred.resolve(res);
})
.error(function(err, status) {
deferred.reject(err)
});
return deferred.promise;
}
return deferred.promise;
}
}
});
This will exactly make only 1 HTTP request.
My issue was that I didn't want to wait for resolve before loading another controller because it would show a "lag" between controllers if the network is slow. My working solution is passing a promise between controllers via ui-router's params and the data from promise can be loaded asynchronously in the second controller as such:
app.route.js - setting the available params to be passed to SearchController, which shows the search results
.state('search', {
url: '/search',
templateUrl: baseDir + 'search/templates/index.html',
controller: 'SearchController',
params: {
searchPromise: null
}
})
landing.controller.js - controller where the user adds search input and submits
let promise = SearchService.search(form);
$state.go('search', {
searchPromise: promise
});
search.service.js - a service that returns a promise from the user input
function search(params) {
return new Promise(function (resolve, reject) {
$timeout(function() {
resolve([]) // mimic a slow query but illustrates a point
}, 3000)
})
}
search.controller.js - where search controller
let promise = $state.params.searchPromise;
promise.then(r => {
console.log('search result',r);
})

Resources