I am new to promise concepts and I'm facing a problem while I'm trying to submit a login form.
When the promise is resolved, I want to update the UI by changing the value of a scope variable.
Here is the controller method which is called
$ctrl.loginForm = function(isValid) {
// check to make sure the form is completely valid
if (isValid) {
LoginService.login($ctrl.user).then(function (value) {
$ctrl.loginError = false;
var cookie = value.token;
}, function (reason) {
$ctrl.loginError = true;
});
}
};
and here is my service method
service.login = function(credentials) {
return new Promise((resolve, reject) => {
$http.post(AppConst.Settings.apiUrl + 'api-token-auth/',credentials).success((data) => {
resolve(data);
}).error((err, status) => {
reject(err, status);
});
});
};
So once the promise is resolved the $ctrl.loginError in the controller changes its value but it does not update on the UI.
I've tried with $apply and it works, but I don't understand why I must use $apply! I thought that the digest cycle starts by default!
Am I doing something wrong?
$http methods return you a promise which is a $q service. So, problem is that you use native promise wrapper which doesn't tell angular's scope to run $apply method.
Change your service to following
service.login = function(credentials) {
return $http.post(AppConst.Settings.apiUrl + 'api-token-auth/',credentials);
};
Related
I'm curious about the best way to spy on dependencies so I can make sure that their methods are being called in my services. I reduced my code to focus on the problem at hand. I'm able to test my service fine, but I want to also be able to confirm that my service (In this case metricService) has methods that are also being called. I know I have to use createSpyObj in some way, but while the function is executing properly, the spyObj methods are not being caught. Should I even be using createSpyObj? Or should I use spyObj? I'm a but confused about the concept of spying when it concerns dependencies.
UPDATE: When using SpyOn I can see one method getting called, but other methods are not
Test.spec
describe("Catalogs service", function() {
beforeEach(angular.mock.module("photonServicesCommons"));
var utilityService, metricsService, loggerService, catalogService, localStorageService;
var $httpBackend, $q, $scope;
beforeEach(
inject(function(
_catalogService_,
_metricsService_,
_$rootScope_,
_$httpBackend_
) {
catalogService = _catalogService_;
$scope = _$rootScope_.$new();
$httpBackend = _$httpBackend_;
$httpBackend.when('GET', "/ctrl/catalog/all-apps").respond(
{
catalogs: catalogs2
}
);
metricsService = _metricsService_;
startScope = spyOn(metricsService, 'startScope')
emitSuccess = spyOn(metricsService, 'emitGetCatalogSuccess').and.callThrough();
endScope = spyOn(metricsService, 'endScope');
})
);
afterEach(function(){
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
describe('get catalog', function(){
it("Should get catalogs", function(done) {
catalogService.normalizedDynamicAppList = testDynamicAppList1;
catalogService.response = null;
var promise3 = catalogService.getCatalog();
promise3.then(function (res) {
expect(res.catalogs).toEqual(catalogs2);
});
expect(metricsService.startScope).toHaveBeenCalled();
expect(metricsService.emitGetCatalogSuccess).toHaveBeenCalled();
expect(metricsService.endScope).toHaveBeenCalled();
$scope.$digest();
done();
$httpBackend.flush();
});
});
});
Service
public getCatalog(): IPromise<Interfaces.CatalogsResponse> {
if (this.response !== null) {
let finalResponse:any = angular.copy(this.response);
return this.$q.when(finalResponse);
}
return this.$q((resolve, reject) => {
this.metricsService.startScope(Constants.Photon.METRICS_GET_CATALOG_TIME);
this.$http.get(this.catalogEndpoint).then( (response) => {
let data: Interfaces.CatalogsResponse = response.data;
let catalogs = data.catalogs;
if (typeof(catalogs)) { // truthy check
catalogs.forEach((catalog: ICatalog) => {
catalog.applications.forEach((application: IPhotonApplication) => {
if( !application.appId ) {
application.appId = this.utilityService.generateUUID();
}
})
});
} else {
this.loggerService.error(this.TAG, "Got an empty catalog.");
}
this.response = data;
this.metricsService.emitGetCatalogSuccess();
console.log("CALLING END SCOPE");
this.metricsService.endScope(Constants.Photon.METRICS_GET_CATALOG_TIME);
resolve(finalResponse);
}).catch((data) => {
this.loggerService.error(this.TAG, "Error getting apps: " + data);
this.response = null;
this.metricsService.emitGetCatalogFailure();
reject(data);
});
});
} // end of getCatalog()
Instead of using createSpyObj, you can just use spyOn. As in:
beforeEach(
inject(function(
_catalogService_,
_$rootScope_,
_$httpBackend_,
_metricsService_ //get the dependecy from the injector & then spy on it's properties
) {
catalogService = _catalogService_;
metricsService = _metricsService_;
$scope = _$rootScope_.$new();
...
// create the spy object for easy referral later on
someMethodSpy = jasmine.spyOn(metricsService, "someMethodIWannaSpyOn")
})
);
describe('get catalog', function(){
it("Should get catalogs", function(done) {
catalogService.normalizedDynamicAppList = testDynamicAppList1;
catalogService.response = null;
var promise3 = catalogService.getCatalog();
...other expects
...
//do the spy-related expectations on the function spy object
$httpBackend.flush(); // this causes the $http.get() to "move forward"
// and execution moves into the .then callback of the request.
expect(someMethodSpy).toHaveBeenCalled();
});
});
I've used this pattern when testing complex angular apps, along with wrapping external imported/global dependencies in angular service wrappers to allow spying and mocking them for testing.
The reason that createSpyObject won't work here is that using it will create a completely new object called metricService with the spy props specified. It won't be the same "metricService" that is injected into the service being tested by the angular injector. You want to get the actual same singleton service object from the injector and then spy on it's properties.
The other source of dysfunction was the $httpBackend.flush()s location. The $httpBackend is a mock for the $http service: you pre-define any number of expected HTTP requests to be made by the code you are testing. Then, when you call the function that internally uses $http to make a request to some url, the $httpBackend instead intercepts the call to $http method (and can do things like validate the request payload and headers, and respond).
The $http call's then/error handlers are only called after the test code calls $httpBackend.flush(). This allows you to do any kind of setup necessary to prep some test state, and only then trigger the .then handler and continue execution of the async logic.
For me personally this same thing happens every single time I write tests with $httpBackend, and it always takes a while to figure out or remember :)
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 have the simplest angular controller:
tc.controller('PurchaseCtrl', function () {
var purchase = this;
purchase.heading = 'Premium Features';
this.onSuccess = function (response) {
console.log('Success', response);
lib.alert('success', 'Success: ', 'Loaded list of available products...');
purchase.productList = response;
};
this.onFail = function (response) {
console.log('Failure', response);
};
console.log('google.payments.inapp.getSkuDetails');
lib.alert('info', 'Working: ', 'Retreiving list of available products...');
google.payments.inapp.getSkuDetails(
{
'parameters': {'env': 'prod'},
'success': purchase.onSuccess,
'failure': purchase.onFail
});
});
And the view:
<div class="col-md-6 main" ng-controller="PurchaseCtrl as purchase">
{{purchase}}
</div>
This prints out:
{"heading":"Premium Features"}
I thought that when the callback returned, the view would be update with any new data. Am I missing something? The callback returns and I see the dtaa in the console.
Using the $scope pattern I think that I would use $scope.$apply to async method, but I'm not sure how to do that here.
Using controllerAs does not change the way digest cycle works or anything. It is just a sugar that adds a property (with the name same as alias name when used) to the current scope with its value pointing to the controller instance reference. So you would need to manually invoke the digest cycle (using scope.$apply[Asyc]() or even with a dummy $timeout(angular.noop,0) or $q.when() etc) in this case as well. But you can avoid injecting scope by abstracting it out into an angular service and returning a promise from there, i.e
myService.$inject = ['$q'];
function myService($q){
//pass data and use it where needed
this.getSkuDetails = function(data){
//create deferred object
var defer = $q.defer();
//You can even place this the global variable `google` in a
//constant or something an inject it for more clean code and testability.
google.payments.inapp.getSkuDetails({
'parameters': {'env': 'prod'},
'success': function success(response){
defer.resolve(response);// resolve with value
},
'failure': function error(response){
defer.reject(response); //reject with value
}
});
//return promise
return defer.promise;
}
}
//Register service as service
Now inject myService in your controller and consume it as:
myService.getSkuDetails(data).then(function(response){
purchase.productList = response;
}).catch(function(error){
//handle Error
});
I have a special developer button on my form, so I don't have to enter the form data every time I am testing the app. The fake button calls the function fakeSubmit() and pre-fills the form.
fakeSubmit simply sets the form value local.code to '000000' and calls the actual submit() function:
scope.submit = function () {
if (scope.requestTicketForm.$invalid) {
scope.local.showErrorAlert = true;
}
else {
// Send request
myApi.getTicket(scope.local.code)
.then(function (data) {
scope.local.showErrorAlert = false;
})
.catch(function (error) {
scope.local.showErrorAlert = true;
});
}
};
scope.fakeSubmit = function () {
$log.info('enter fake request click');
scope.local.code = '000000';
scope.submit();
};
The problem now is that the form $invalid property is not updated immediately, so the submit function sets showErrorAlert to true instead of sending the request. I tried adding a scope.$apply before calling submit():
scope.fakeSubmit = function () {
$log.info('enter fake request click');
scope.local.code = '000000';
scope.$apply();
scope.submit();
};
This works, but it throws the following error:
Error: [$rootScope:inprog] $apply already in progress
Any ideas how to solve this?
Another way of forcing angular to update the scope is to call $digest:
if(!$scope.$$phase) {
$scope.$digest();
}
So your code should be:
scope.fakeSubmit = function () {
$log.info('enter fake request click');
scope.local.code = '000000';
if(!scope.$$phase) {
scope.$digest();
}
scope.submit();
};
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.