I'm trying to create a service that will hold the shopping cart content of my website using AngularJS. I will then use this to make sure the reference to the cart in all controllers etc should be to the same object and synced.
The problem is that the cart content must be initialized via an ajax call. My code below does not work but it shows what I'm trying to accomplish. I would like to somehow get the list of items and return with getItems(), but if the list of items is not yet fetched then I will need to fetch for it first and promise a return. I'm trying to wrap my head around the "promise" concept but so far I have not fully got it yet.
factory('cartFactory', function ($rootScope, Restangular) {
var cart = $rootScope.cart = {};
return {
getItems: function () {
if (undefined == cart.items) {
return Restangular.oneUrl('my.api.cart.show', Routing.generate('en__RG__my.api.cart.show')).get().then(function($cart){
cart = $rootScope.cart = $cart;
angular.forEach(cart.items, function(value, key){
cart.items[key]['path'] = Routing.generate('en__RG__my.frontend.product.info', {'slug': value.variant.product.slug});
});
return cart.items;
});
} else {
return cart.items
}
},
setItems: function ($items) {
cart.items = $items;
},
removeItem: function ($item) {
cart.splice(cart.indexOf($item), 1);
},
addItem: function ($item) {
cart.items.push($item)
}
}
})
I will try to explain this in a very simplified way.
A promises is just an object that is "passed around" and we use this objects to attach functions that will be executed whenever we resolve, reject or notify the promise.
Because in Javascript objects are passed by reference we are able to refer to the same object in several places, in our case inside the service and the controller.
In our service we execute:
getItems: function () {
var deferred = $q.defer();
// do async stuff
return deferred.promise;
}
Lets say that the variable deferred above is an object more os less like this:
{
reject: function (reason) {
this.errorCallback(reason);
},
resolve: function (val) {
this.successCallback(val);
},
notify: function (value) {
this.notifyCallback(value);
},
promise: {
then: function (successCallback, errorCallback, notifyCallback) {
this. successCallback = successCallback;
this.errorCallback = errorCallback;
this.notifyCallback = notifyCallback;
}
}
}
So when we call getItems() a promise (deferred.promise) is returned and this allows the callee to set the callbacks to be executed whenever the promise changes its state (resolve, reject or notify).
So inside our controller I am setting only the resolve callback, if the promises is rejected it will happen silently because there is no errorCallback to be executed.
cartFactory.getItems().then(function (items) {
$scope.items = items;
});
Of course there is much more behind it, but I think this simplistic promise will help you get the basic idea. Be aware that cartFactory.getItems() must always return a promise, even when the items are already loaded, otherwise cartFactory.getItems().then() would break if , for example, you return an array.
Here a JSBin with your cartFactory service, I am using $timeout to simulate an async call.
Hope this helps.
Related
I have one Angular.js method from child controller, where it makes a call to twop parent controller methods one after another. But the second function should get the account data from from the first function and then will update call data like below:
filter.filterAccountsByProductMetrics1 = function(productWithSegmentations12) {
accountService.fetchAccountForRecordType([filter.selectedHcpHco.Name.display])
.then(function(resp) {
$scope.accountDataUpdate({
accounts: resp.data
});
var productId = null;
if(filter.selectedMySetupProduct.Product_vod__c) {
productId = filter.selectedMySetupProduct.Product_vod__c.value;
}
callService.getCallsForProductId(productId)
.then(function(calls) {
filter.filterRecords[filterType.product.value] = calls;
$scope.callDataUpdate({
calls: applyAllFilterOnCalls()
});
});
});
};
I've checked both the functions are getting called but the sequence is not maintained. How to make sure the two parent functions get called one after another.
EDIT: function accountDataUpdate:
call.accountDataUpdate = function(accounts) {
call.accounts = accounts;
getCallDetails(getCallIdsFromCallsForFilteredAccount())
.then(function() {
updateProductFrequencyTableData();
updateAccountDetailData(true);
});
updateDailyFrequencyChartData();
updateWeeklyFrequencyChartData();
updateCallFrequencyTableData();
updateAccountFrequencyData();
$timeout(function() {
$scope.$broadcast('updateDoughnutChart');
$scope.$broadcast('updateBarChart');
});
};
Modify accountDataUpdate to return a promise:
call.accountDataUpdate = function(accounts) {
call.accounts = accounts;
var promise = getCallDetails(getCallIdsFromCallsForFilteredAccount())
.then(function() {
updateProductFrequencyTableData();
updateAccountDetailData(true);
updateDailyFrequencyChartData();
updateWeeklyFrequencyChartData();
updateCallFrequencyTableData();
updateAccountFrequencyData();
return $timeout(function() {
$scope.$broadcast('updateDoughnutChart');
$scope.$broadcast('updateBarChart');
});
});
return promise;
};
Then use that promise for chaining:
filter.filterAccountsByProductMetrics1 = function(productWithSegmentations12) {
return accountService.fetchAccountForRecordType([filter.selectedHcpHco.Name.display])
.then(function(resp) {
return $scope.accountDataUpdate({
accounts: resp.data
});
}).then(function() {
var productId = null;
if(filter.selectedMySetupProduct.Product_vod__c) {
productId = filter.selectedMySetupProduct.Product_vod__c.value;
}
return callService.getCallsForProductId(productId)
}).then(function(calls) {
filter.filterRecords[filterType.product.value] = calls;
return $scope.callDataUpdate({
calls: applyAllFilterOnCalls()
});
});
};
Because calling the .then method of a promise returns a new derived promise, it is easily possible to create a chain of promises.
It is possible to create chains of any length and since a promise can be resolved with another promise (which will defer its resolution further), it is possible to pause/defer resolution of the promises at any point in the chain. This makes it possible to implement powerful APIs.
For more information, see AngularJS $q Service API Reference - Chaining promises.
I am using 2 service in controller
First Service is to get AjaxResponse where logic to fetching the response is mentioned
The second Service calls the first service to make Http request and get result and then, in turn, return it to the controller
Ctrl Dependency injected firstService,secondService
this.getService = secondService.getData(param);
First Service--> firstService
this.httpResponse(param){
var re = $http.get(param);
return re.then(success,fail);
}
function success(data){
return data;
}
function fail(data){
console.log(data);
}
Second Service (Dependency injection of First Service)
function secondService(firstService){
this.getData = function(param){
return firstService.httpResponse(param);
};
}
this.getService is coming as undefined, all the call are going properly.
Even tried the following code:
secondService.getData(param).then(function(data){console.log(data);});
That doesn't help either.
You should chain the promises in these kinds of situations.
First, you define your service. It should contain two distinct functions. As an example, I did a GET and a POST.
angular.module("myApp",[]).factory("SharedServices", function($http) {
return {
getItem: function() {
return $http.get('path/to/api');
},
postItem: function(payload) {
return $http.post('path/to/api', payload);
}
};
});
Then, reference the service in your controller. getItem() will return a promise, where you can use it using .then to call your second service in the success callback.
angular.module("myApp",[]).controller("MainCtrl", function($scope, SharedServices) {
SharedServices.getItem().then(function(response) {
//success, got something
//call the second part
var payload = { myPayload: response.data.someItem };
SharedServices.postItem(payload).then(function(response) {
//success
}, function(response) {
//an error has occurred to the second call--POST
});
}, function(response) {
//an error occurred to the first call--GET
});
});
Used Callback to get the result.It is similar to deferred(promise)
NB: Code reproduced from memory.
I have a method generated by djangoAngular that has this signature in my service:
angular.module('myModule')
.service('PythonDataService',['djangoRMI',function(djangoRMI){
return {getData:getData};
function getData(foo,bar,callback){
var in_data = {'baz':foo,'bing':bar};
djangoRMI.getPythonData(in_data)
.success(function(out_data) {
if(out_data['var1']){
callback(out_data['var1']);
}else if(out_data['var2']){
callback(out_data['var2']);
}
}).error(function(e){
console.log(e)
});
};
}])
I want to test my service in Jasmine, and so I have to mock my djangoAngular method. I want to call through and have it return multiple datum.
This is (sort of) what I have tried so far, reproduced from memory:
describe('Python Data Service',function(){
var mockDjangoRMI,
beforeEach(module('ng.django.rmi'));
beforeEach(function() {
mockDjangoRMI = {
getPythonData:jasmine.createSpy('getPythonData').and.returnValue({
success:function(fn){fn(mockData);return this.error},
error:function(fn){fn();return}
})
}
module(function($provide) {
$provide.provide('djangoRMI', mockDjangoRMI);
});
});
it('should get the data',function(){
mockData = {'var1':'Hello Stackexchange'};
var callback = jasmine.createSpy();
PythonDataService.getData(1,2,callback);
expect(callback).toHaveBeenCalled();
})
})
But when I put another it block in with a different value for mockData, only one of them is picked up.
I'm guessing that because of the order of operation something is not right with how I'm assigning mockData. How can I mock multiple datum into my djangoRMI function?
Ok, this is a re-posting of a previous question which I'd confused by over-simplifying the code...
I have an angularjs factory function which queries a parse database, and returns a promise ... so far, pretty simple... however, my controller has a foreach loop which calls the function for each of a range of ids, which are meant to then resolve concurrently. The resolve doesn't work, however, and what I get back appears to be a garbled mixture of the api calls.
Here's a simplified version of my code...
app.factory('MyFactory', function($http, $q ) {
return {
testFunction: function (ingredientList) {
var deferred = $q.defer()
ingredientIndex=-1
var regulariseIngredient=function() {
ingredientIndex++
if (ingredientIndex>ingredientList.length-1) {
deferred.resolve(ingredientList);
return
}
(new Parse.Query("Ingredient"))
.equalTo("type", ingredientList[ingredientIndex].type)
.equalTo("name", ingredientList[ingredientIndex].name)
.include("parent")
.find().then(function(result) {
result=result[0]
if(result.get("parent")) result=result.get("parent")
ingredientList[ingredientIndex]=result
regulariseIngredient();
})
}
regulariseIngredient()
return deferred.promise
}
}
}
app.controller('TestController', function($scope,MyFactory) {
recipes = [ An array of objects, each with an ingredients array]
angular.forEach(recipes, function(recipe) {
MyFactory.testFunction(recipe.ingredients).then(function(result) {
console.log(result)
})
})
}
Bit more info on what the service actually does...
my app has a collection of recipes, and a collection of ingredients. Each ingredient has a 'parent' which substitutes it in the 'rationalised' version. The test function loops through each ingredient, compiles the outcome and then returns a collection of substituted ingredients. The controller is looping through the recipes, so that I can check the ingredients on hand of all recipes.
#MarkPeace, it appears that you have hit on the anti-pattern "outlawed" here under the heading "The Collection Kerfuffle".
The espoused pattern (for performing an asynchronous task, in parallel, on each member of an array of items) is :
function workMyCollection(arr) {
return q.all(arr.map(function(item) {
return doSomethingAsync(item);
}));
}
In your case, doSomethingAsync() is (new Parse.Query("Ingredient"))...find();
In full, the code will be something like :
app.factory('MyFactory', function($http, $q) {
return {
testFunction: function (ingredientList) {
return $q.all(ingredientList.map(function(item) {
return (new Parse.Query("Ingredient"))
.equalTo("type", item.type)
.equalTo("name", item.name)
.include("parent")
.find().then(function(result) {
return result[0].get("parent") || result[0];//phew, that's a bit more concise
});
}));
}
}
});
testFunction() will thus return a promise of an array of results (whatever they happen to be).
In the controller, you can use the same pattern again to loop through the recipes :
app.controller('TestController', function($scope, MyFactory) {
var recipes = [ An array of objects, each with an ingredients array ];
$q.all(recipes.map(function(recipe) {
return MyFactory.testFunction(recipe.ingredients).then(function(results) {
console.dir(results);
return results;
});
})).then(function(arr) {
//all done-and-dusted
//If everything has been successful, arr is an array of arrays of results
//Do awesome stuff with the data.
})['catch'](function(err) {
//report/handle error here
});
});
Of course, exactly how much you do in the controller and how much in the factory (or factories) is up to you. Personally, I would choose to keep the controller somewhat simpler.
Using $q.all() which takes an array of promises as argument and returns a new promise which is:
resolved if all promises of the array are resolved
rejected if any of the promise of the array is rejected
from the doc:
Returns a single promise that will be resolved with an array/hash of values, each value corresponding to the promise at the same index/key in the promises array/hash. If any of the promises is resolved with a rejection, this resulting promise will be rejected with the same rejection value.
For the loop, using Array.forEach is cleaner:
testFunction: function (ingredientList) {
var promiseArray = [];
ingredientList.forEach(function (ingredient) {
var promise = (new Parse.Query("Ingredient"))
.equalTo("type", ingredient.type)
.equalTo("name", ingredient.name)
.include("parent")
.find().then(function(result) {
result = result[0]
if(result.get("parent")) {
result = result.get("parent");
}
return result;
});
promiseArray.push(promise);
});
return $q.all(promiseArray);
}
I'd like my objects to cache the result of some network requests and answer the cached value instead of doing a new request. This answer here done using angular promises looks a lot like what I'm going for, but I'm not sure how to express it using the Parse.com promise library. Here's what I'm trying...
module.factory('CustomObject', function() {
var CustomObject = Parse.Object.extend("CustomObject", {
cachedValue: null,
getValue: function() {
if (this.cachedValue) return Parse.Promise.as(this.cachedValue);
return this.functionReturningPromise().then(function (theValue) {
this.cachedValue = theValue;
return this.cachedValue;
});
},
My idea is to return a promise whether or not the value is cached. In the case where the value is cached, that promise is resolved right away. The problem is, as I follow this in the debugger, I don't seem to get the cached result on the second call.
Your value is almost correct. Your design is correct the only issue you have here is dynamic this.
In the context of the .then handler, this is set to undefined (or the window object), however - since you're using Parse promises and I'm not sure those are Promises/A+ compliant it can be arbitrary things - the HTTP request, or whatever. In strict code and a good promise library - that would have been an exception.
Instead, you can do CustomObject.cachedValue explicitly instead of using this:
var CustomObject = Parse.Object.extend("CustomObject", {
cachedValue: null,
getValue: function() {
if (CustomObject.cachedValue) return Parse.Promise.as(this.cachedValue);
return this.functionReturningPromise().then(function (theValue) {
CustomObject.cachedValue = theValue;
return this.cachedValue;
});
},
If $q promises are also possible instead of Parse promises, I'd use those instead:
var cachedValue = null;
getValue: function() {
return $q.when(cachedValue || this.functionReturningPromise()).then(function(theValue){
return cachedValue = theValue;
});
}
You can just cache the promise and return that
module.factory('CustomObject', function() {
var CustomObject = Parse.Object.extend("CustomObject", {
cachedPromise: null,
getValue: function() {
if (!this.cachedPromise) {
this.cachedPromise = this.functionReturningPromise();
}
return this.cachedPromise;
},
...
}
...
}
I am not familiar with the Parse.com promise library, but it could be a plain JS error:
The this inside the function is not referring to the Promise object, but to the global object.
Change the code like that:
...
getValue: function() {
if (this.cachedValue) return Parse.Promise.as(this.cachedValue);
var that = this;
return this.functionReturningPromise().then(function (theValue) {
that.cachedValue = theValue;
return that.cachedValue;
});
},