Please see below the structure of my angular application:
I have a page called 'firm.html' which contains a button. Clicking this button executes the code below.
Controller
The controller calls a Service function. The generationInProgress variable is used in an ng-show to toggle the visibility of a loading gif on the HTML page
$scope.generationInProgress = true;
firmService.processFirm(firmRequest).then(function(response) {
window.location.href = "firm/process";
$scope.generationInProgress = false;
});
Firm Service
This is a service that handles the Firm operations with the following function called above
this.processFirm = function(firmRequest) {
return httpService.put('firm/process', firmRequest);
};
HTTP Service
This is a service that handles all calls to the service. It is used by multiple services, including the firmService above. Here is the put method as called above
this.put = function(url, data) {
return promise = $http.post(url, data).success(function(response) {
return response;
}).error(function(response) {
console.log("error");
});
};
If a HTTP error code is returned by the server, obviously the .error function is executed. If I had a dedicated error page, I could just redirect to that page.
However, I need to display the error on the 'firm.html' page while also setting the $scope.generationInProgress back to false so that the loading gif is no longer displayed. None of the code that does this can be located within the httpService because it is a common service used by many different components.
I am unsure how to propagate the error back to the controller in order to accomplish this. Do I just put return response; in both the .success and .error and use an IF statement in the controller to test for the HTTP code? Is there an alternative method?
Any advice is appreciated.
The .success and .error methods have been deprecated. Instead, use the .then and .catch methods.
To chain a successful promise, return data to the .then method. To chain a rejected promise, throw the error response:
this.put = function(url, data) {
//Use .then method
return promise = $http.post(url, data).then(function(response) {
//return to chain success
return response;
//Use .catch method
}).catch(function(response) {
console.log("error");
//throw to chain rejection
throw response;
});
};
From the Docs1:
Deprecation Notice
The $http legacy promise methods .success and .error have been deprecated. Use the standard .then method instead.
You can handle rejected state in controller, than present it as you like in html.
$scope.generationInProgress = true;
$scope.error = "";
firmService.processFirm(firmRequest).then(function(response) {
window.location.href = "firm/process";
$scope.generationInProgress = false;
}, function(err) {
$scope.generationInProgress = false;
$scope.error = "Your customized error message. Caused by: " + err;
});
Related
I want to make an $http.post inside another $http.post as the second one is dependant on the first. Basically what I'm doing is:
$http.post("/my/server/location").then(function (response) {
$http.post("/my/second/api/call/"+response.data.number).then(function (response) {
$scope.message = "Created successfully";
}, function (response){
$scope.message = "Could not create";
});
//Create modal from response.data received from first API Call
//Add $scope.message to this modal.
});
What's happening as you would have guessed is the second one stays pending until the initial promise is returned, and I want to show $scope.message in the modal which is hence, not possible. While I understand why that's happening, I can't seem to figure how to get around that. I tried dealing with $q but made a mess out of it. Any help will be gladly appreciated.
To chain the message use a return statement in both the success handler and the rejection handler of the second XHR:
$http.post("/my/server/location").then(function (response1) {
return $http.post("/my/second/api/call/"+response1.data.number)
.then(function (response2) {
$scope.message = "Created successfully";
return $scope.message;
}).catch(function (errorResponse2) {
$scope.message = "Could not create";
return $scope.message;
}).then(function(message) {
//Create modal from response1.data received from first API Call
//Add `message` from the second XHR to this modal.
return [response1.data, message];
});
});
The return statement in the .catch rejection handler converts the rejected promise to a success which is handled by the next .then method in the chain.
The final promise returns an array which has both the data from the first XHR and the message from the second XHR.
Chaining Promises
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.
— AngularJS $q Service API Reference - Chaining Promises
You're right, $q won't help here unless you want both promises to be executed at once and then wait for both of their responses.
Here you want to execute one promise then the other, which according to your code looks fine. However, anything you want to do after the second promise has executed needs to be done in that .then() block.
Example:
$http.post("/my/server/location").then(function (response) {
$http.post("/my/second/api/call/"+response.data.number).then(function (response) {
$scope.message = "Created successfully";
// Create modal from response.data received from first API Call
}, function (error){
$scope.message = "Could not create";
});
});
$http.post("/my/server/location")
.then(function (response) {
$http.post("/my/second/api/call/"+response.data.number).then(function
(response) {
}, function (response){
});
$scope.message = "Created successfully";
//Create modal from response.data received from first API Call
//Add $scope.message to this modal.
$scope.buildModal(response.data);
})
.catch(function(error){
$scope.message = "Could not create";
$scope.buildModal();
})
So the issue with what you had was that $scope.message = "Created successfully"; was locked in the nested api call. By taking it out of that you can build your modal immediately after the first api call. In the code above, nothing is waiting for the second api call to finish because nothing is in its .then block
I moved the modal building to a separate function buildModal because it has to be called both from the .then and the .catch block
Try this
var fn = {
test: function() {
return $http.post("/my/server/location");
}
};
fn.test().success(function(response) {
$http.post("/my/second/api/call/"+response.data.number).then(function (response) {
$scope.message = "Created successfully";
}, function (response){
$scope.message = "Could not create";
}
});
We have a service, lets call it AccountService which exposes a method called getAccounts(customerId) among others.
In its implementation all it does is to fire up a $http GET request and return a promise to the calling controller which will put the returned array of accounts in the controller scope once resolved.
On a simplified view all looks like below:
// The service
.factory('AccountService', ['$http', function($http) {
var _getAccounts = function(customerId) {
var request = {
'method': 'GET',
'url': 'http://localhost:8081/accounts/' + customerId
};
return $(request);
};
return {
getAccounts: _getAccounts
};
}]);
// Inside the conntroller
AccountService.getAccounts($scope.customerId)
.then(function(response) {
$scope.accounts = response.data;
});
So once the promise will resolve the controller scope will get populated with the list of accounts.
Note that I kept the above code as simple as I could to get you the idea of what my problem is but in reality it will be code to deal with exceptions, watcher to refresh, etc. Everything works fine.
My problem is that this AccountService is used from lots of controllers and putting the promise resolve in all of these looks to me not only repeating all this boiler plate resolver code but also complicating the unit testing as I am obliged to r/test both successful and exception scenarios in every single controller test.
So my question is:
Is there a nice way to resolve the promise in the service and return the response to the controller, not the promise?
Please note I am a very beginner with Angular and JS so please be gentle if my question looks naive. I have heaps of java experience and my mind seems to go java like everywhere which may not be the case.
Thank you in advance for your inputs
To answer your original question:
Is there a nice way to resolve the promise in the service and return the response to the controller, not the promise?
In my opinion, no, there isn't. It boils down to the way asynchronous calls work - you either pass a callback (and the method returns nothing), or you don't pass a callback and the method returns an object which will be notified (a promise). There may be some workarounds, but I don't think it gets nicer than that.
One way to partially reduce the boilerplate is to use a catch in the service, and return the promise returned by it instead.
Consider the following extremely simplified example:
angular.module('myApp')
.factory('NetworkRequests', [
function() {
var _getData = function() {
var promise = new Promise((resolve, reject) => {
var a = true,
data = ['a', 'b', 'c'];
if (a) {
resolve(data);
} else {
reject('Rejection reason: ...');
}
});
return promise.catch((error) => {
// Notify some error handling service etc.
console.log(error);
return [];
});
};
return {
getData: _getData
};
}
]);
The promise variable would be the result from your http request. You should return some data in the catch function that makes sense in the controller context (e.g. empty array). Then you don't have to bother with error handling in the controller:
angular.module('myApp')
.controller('DataController', ['NetworkRequests',
function(NetworkRequests) {
NetworkRequests.getData().then((data) => {
this.data = data;
});
}
]);
Again, this doesn't solve the complete issue, but at least the error handling part can be encapsulated in the service.
You can design in such a way that once your $http is done with fetching the data, store it your factory variable (somewhat a cache), and for subsequent factory calls, you check if the cache has such data. If yes, return the cache data, else call the $http calls.
Here is the code:
.factory('AccountService', ['$http', '$q', function($http, $q) {
var cachedData = null;
var defered = $q.defer(); //create our own defered object
var _getAccounts = function(customerId) {
if (cachedData !== null) {
console.log('get from cachedData')
defered.resolve(cachedData); // resolve it so that the data is passed outside
return defered.promise; //return your own promise if cached data is found
} else {
var request = {
'method': 'GET',
'url': 'mockdata.json'
};
return $http(request).then((response) => { //return a normal $http promise if it is not.
console.log('get from $http');
cachedData = response.data;
return cachedData;
});
}
};
return {
getAccounts: _getAccounts
};
}]);
Here is the working plnkr. You can open up the console, and click the GetData button. You will see that first time it logs get from $http, where as subsequent calls it logs get from cachedData.
One way is to reuse an object and fill it with data. It is used by ngResource.
It is something like
var data = [];
function getAccounts(customerId) {
var promise = $http(...).then((response) => {
Object.assign(promise.data, response.data)
});
promise.data = [];
return promise;
};
Data is available for binding as $scope.accounts = AccountService.getAccounts(...).data. The obvious drawback is that there is a splash of unloaded content.
Another way is the one you've mentioned. It is being used most frequently. If there is a problem with WET code in controllers, it should be treated by eliminating WET code with class inheritance, not by changing the way it works.
Yet another way is the recommended one. Using a router and route/state resolvers eliminates the need for asynchronously loaded data. The data resolved in resolver is injected into route template as an array.
I have gone through a few replies about using $http service for accessing the properties file, but now sure how it would fit in this scenario
I have created a service that returns the hostnames from the poperties file, the calling client to this service should make a blocking call to the service and proceed only if the property file is read.
var serviceMod = angular.module('serviceModule',[])
.factory('configService', function($http){
return {
getValue: function(key){
$http.get("js/resources/urls.properties").success(function(response){
console.log('how to send this response to clients sync??? ' + response)
})
return ????
}
}
})
someOtherControllr.js
var urlValue = configService.getValue('url')
The problem I am facing is to do with the aync nature of the $http service. By the time the response is received by the callback, the main thread is already finished executing the someOtherController.js
You need to resolve the promise returned by the service. We can just return the $http call and resolve it in our controller (since return $http.get be a promise itself). Check out the AngularJS $q and $http docs for a bettering understanding of the underlying mechanics going on, and observe the following change...
.factory('configService', function($http) {
return {
getValue: function(key) {
return $http.get('js/resources/urls.properties');
}
}
});
var urlValue;
// --asynchronous
configService.getValue('url').then(function(response) {
urlValue = response.data; // -- success logic
});
console.log('be mindful - I will execute before you get a response');
[...]
Simple way - use callback (it will still be async. In fact you cant make it sync) :
getValue: function(key, onSuccess){
$http.get("js/resources/urls.properties").success(function(response){
onSuccess(response);
})
I'm using AngularJS application. I have a form. On submit I'm calling a function. I have used the javascript try/catch/finally block
$scope.save = function() {
try {
//Block of code to try
$scope.submit.text = "Submitting";
$scope.submit.disable = true;
$timeout(function(){
alert('successfully saved');
}, 5000);
}
catch(err) {
//Block of code to handle errors
}
finally {
alert("finally");
$scope.submit.text = "Submit";
$scope.submit.disable = false;
}
}
I used a timer for now. But later I may use AJAX call. the problem is
finally block gets executed before the time finishes. How to solve this?
The reason why the finally method gets executed before the time is finished is that javascript methods are not running asynchronous. Promises will solve this problem.
Here is how promise is defined in q.js which is used in angular:
A promise is an object that represents the return value or the thrown
exception that the function may eventually provide. A promise can also
be used as a proxy for a remote object to overcome latency.
One of the guarantees promises make is that the either the success or the error callback will be invoked, but never both. What happens if you need to ensure a specific function executes regardless of the result of the promise? You can do this by registering that function on the promise using the finally() method.
Suppose you have a function getData() where you are making some $http request and obtain some data from backend. Then you can use:
var promise = getData()
.then(function(data) {
console.log(data)
}, function(error) {
console.error(error)
})
.finally(function() {
console.log()
})
})
I have a factory which wraps $http for custom template loading, and I'm trying to write unit tests for it. Valid templates work because they're pre-loaded into $templateCache, so the underlying http promise never needs to be called.
However, I'm trying to test our promise gets rejected if the file path is wrong.
I'm telling ngMock to respond with a 404:
$httpBackend.when('GET', '/invalid').respond(404, '');
My test never fires the then/catch/finally code. The underlying http promise is never resolved/rejected.
I've reduced the test to pure $http calls while resolving this issue.
it('"get" rejects with invalid path', function(done) {
var deferred = $http.get('/invalid');
deferred.catch(function(error) {
console.log(error);
done(error);
});
deferred.then(function(response) {
console.log(response);
done();
});
deferred.finally(function() {
console.log('finally!');
});
// Trigger http promise resolution
$rootScope.$digest();
});
In order to trigger the responses to requests, call $httpBackend.flush();. This will also trigger a digest cycle, so you won't need to call .$apply();.
Also, the response from an $http is a modified promise. I've updated the code below to handle your use case.
Lastly, make sure that you are using the ngMock module and not the ngMockE2E. They each give you a slightly different $httpBackend object, but the ngMock module is meant for unit testing. See the differences here (ngMock's $httpBackend vs ngMockE2E's $httpBackend)
Here is a simplified fiddle showing how it works.
Your updated code would be:
it('"get" rejects with invalid path', function() {
$httpBackend.whenGET('/invalid').respond(404, '');
var wasRejected = false;
$http.get('/invalid')
.error(function (data, status, headers, config) {
console.log(status);
wasRejected = true;
});
// Trigger http promise resolution
$httpBackend.flush();
// Make your assertion...
expect(wasRejected).toBeTrue();
});
afterEach(function(){
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
You have a couple problems with your code. First $http promises are resolved using success, error and then. From the angular documentation here:
$http Returns a promise object with the standard then method and two http specific methods: success and error. The then method takes two arguments a success and an error callback which will be called with a response object. The success and error methods take a single argument - a function that will be called when the request succeeds or fails respectively. The arguments passed into these functions are destructured representation of the response object passed into the then method.
The second is that you need to call $httpBackend.flush(); to trigger the return of your request. This is to simulate an asynchronous behavior in a synchronous test.
it('Should return status Not Found', function() {
$httpBackend.expectGET('/invalid').respond(404, 'Not Found');
var result = '';
$http.get('/invalid')
.success(function(data){
result = data;
}).error(function(reason){
result = reason;
});
$httpBackend.flush();
expect(result).toEqual('Not Found');
});
});