Example of progress on file upload in AngularJS $http POST - angularjs

I am trying to get a 'progress' event from AngularJS $http POST request for file upload.
After looking at $http upload file progress in AngularJS, I came across one recent angular.js git commit, that suppose to resolve the issue Add XHR progress event handling to $http and $httpBackend.
Did anyone achieve this working? And if so, can kindly show the example?
PS. I'd prefer to stay with $http rather than create my own XMLHttpRequest. The reason is that my backend expects to get json object combined with multipart file data. And the attempt to make through XMLHttpRequest is failing with error message that backend doesn't see the json object part of request "Required String parameter 'objData' is not present. The request sent by the client was syntactically incorrect." While in the POST message I see "Content-Disposition: form-data; name="objData"" in Firebug.
$scope.uploadFile = function() {
var url = buildUrl('/upload');
var data = {objData: $scope.data, fileData: $scope.file};
var formData = new FormData();
formData.append("objData", angular.toJson(data.objData));
formData.append("fileData", data.fileData);
var xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener("progress", uploadProgress, false);
xhr.open("POST", url);
xhr.setRequestHeader("Content-Type","application/json;charset=utf-8");
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
xhr.send(formData);
};

At time of writing $http doesn't support the notify method of the new 1.2 $q. So you have to use jquery xhr. Its rather simple once set up:
Notice that we return a promise so your consumer of uploadFile would do uploadFile(..).then(success, fail, progress)
$scope.uploadFile = function() {
var deferred = $q.defer();
var getProgressListener = function(deferred) {
return function(event) {
//do some magic
deferred.notify(magic);
};
};
var formData = new FormData();
formData.append("objData", angular.toJson(data.objData));
formData.append("fileData", data.fileData);
$.ajax({
type: 'POST',
url: buildUrl('/upload'),
data: formData,
cache: false,
// Force this to be read from FormData
contentType: false,
processData: false,
success: function(response, textStatus, jqXHR) {
deferred.resolve(response);
},
error: function(jqXHR, textStatus, errorThrown) {
deferred.reject(errorThrown);
},
xhr: function() {
var myXhr = $.ajaxSettings.xhr();
if (myXhr.upload) {
myXhr.upload.addEventListener(
'progress', getProgressListener(deferred), false);
} else {
$log.log('Upload progress is not supported.');
}
return myXhr;
}
});
return deferred.promise;
};

Related

ExpressJS IP and AngularJS $http get

I'm trying to learn ExpressJS and I'm having trouble getting IP address from an Express route to display in the browser via Angular controller.
I'm using 2 Nodejs modules (request-ip and geoip2) to get the IP and then lookup geolocation data for that IP. Then trying to use Angular to display the geolocation data in the browser using an Angular $http get call.
My Express route for the IP:
// get IP address
router.get('/ip', function (req, res, next) {
console.log('requestIP is ' + ip);
// geolocation
geoip2.lookupSimple(ip, function(error, result) {
if (error) {
//return res.status(400).json({error: 'Something happened'});//default
return res.sendStatus(400).json({error: 'Something happened'});
}
else if (result) {
return res.send(result);
}
});
});
And my AngularJS controller code:
function MainController($http) {
var vm = this;
vm.message = 'Hello World';
vm.location = '';
vm.getLocation = function() {
$http({
method: 'GET',
url: 'localhost:8000/ip'
}).then(function (result) {
console.log(result);
return vm.location = result;
});
};
};
The Hello World message displays but not the location...? I can also go to localhost:8000/ip and see the JSON result. The result doesn't appear in Chrome's console either. The result is a json object like this:
{"country":"US","continent":"NA","postal":"98296","city":"Snohomish","location":{"accuracy_radius":20,"latitude":47.8519,"longitude":-122.0921,"metro_code":819,"time_zone":"America/Los_Angeles"},"subdivision":"WA"}
I'm not sure why the Hello Word displays and the location doesn't when it seems that I have everything configured correctly... so obviously I'm doing something wrong that I don't see...?
You have initialised 'vm.location' as a string when in fact it is a JSON object.
vm.location = {};
You need to adjust the url paramater in your request to:
url: '/ip'
As you are sending back JSON from Express.js, you should change your response line to:
return res.json(result);
Do you call vm.getLocation() somewhere in your code after this?
The data you need is under result.data from the response object.
Also in order to display the data in the html you have to specify which property to display from the vm.location object (vm.location.country, vm.location.city etc..).
From angular docs about $http:
The response object has these properties:
data – {string|Object} – The response body transformed with the transform functions.
status – {number} – HTTP status code of the response.
headers – {function([headerName])} – Header getter function.
config – {Object} – The configuration object that was used to generate the request.
statusText – {string} – HTTP status text of the response.
Is this express js and angular hosted on the same port? If so please replace your
$http({
method: 'GET',
url: 'localhost:8000/ip'
}).then(function (result) {
console.log(result);
return vm.location = result;
});
with
$http({
method: 'GET',
url: '/ip'
}).then(function (result) {
console.log(result);
return vm.location = result;
});
It may be considered as CORS call and you have it probably disabled.
You can also specify second function to then (look code below) and see if error callback is called.
$http({
method: 'GET',
url: '/ip'
}).then(function (result) {
console.log(result);
return vm.location = result;
}, function (error) {
console.log(error);
});

how to mock response.headers('location') in jasmine?

In Angular controller, I have some codes like :
$http.save('api/purchases').$promise.then(function(response) {
var location = response.headers('location'); // to retrieve the location in response header
})
in jasmine's unit test,
it('..', function(){
$httpBackend.expectPost('api/purchase').respond(201, {}, {location: 'xxx'});
// when run the test, it reports an error with 'undefined is not a constructor' (evaluting response.headers('location'))
})
How can i test the response.headers('location')?
The Angular docs seem to indicate that respond needs statusText when given headers (as in it is not optional and is required when passing in headers)
function([status,] data[, headers, statusText])
You should be able to pass some text for a 201 like so
$httpBackend.expectPost('api/purchase').respond(201, {}, {location: 'xxx'}, 'Created');
use transformResponse in $resource action
var resource = $resource('api/purchase', {}, {
get: {
method: 'GET',
transformResponse: function(data, headers) {
var response = {};
response.data = data;
response.headers = headers();
return response;
}
}
}
to retrieve 'location':
resource.get().$promise.then(function(response){
var location = response.headers.location; // 'headers' has been put into response object above
})
then test can run normally.

Restangular save and retry after refresh

I'm working on an SPA that is usually online but can go offline and I have to keep a log of all requests to an API. When certain requests fail I should retry them later.
I've got a local DB to save all requests. When my app starts I retrieve them all and retry the ones marked to.
What I need is a way to config a Restangular object based on what I already sent. I have a response interceptor and I'm saving the restangular response object.
{
config: {
headers: Object,
method: "GET",
params: Object,
transformRequest: Array[1],
transformResponse: Array[1],
url: "..."
},
data: {...},
headers: {...},
status: 200,
statusText: "OK"
}
Is there a function to create a Restangular with the given config object?
Thanks
If i would doing this i would setup setErrorInterceptor
var yourLocalDb = function($http) {
var failedRequests = [];
this.store = function(request) {
this.failedRequests.push(request);
}
this.retry = function() {
var self = this;
angular.forEach(this.failedRequests,function(request) {
$http(request.response.config).then(function() {
//in case of success
self.failedRequests.splice(self.failedRequests.indexOf(request),1);
request.responseHandler.apply(undefined,arguments);
}), request.deferred.reject);
});
}
}
Restangular.setErrorInterceptor(function(response, deferred, responseHandler) {
yourLocalDb.store({
response : response,
responseHandler : responseHandler,
deffered : deffered
});
}
then when you have connection you can just call
yourLocalDb.retry();
Not tested, but it should give you clue.

Setting headers and http params for a single request in restangular

I am trying to use restangular for file upload post request , I want to achieve the same functionality as below in restangular.
However, I was unsure how to set content type and transformRequest for just this particular request. If I understand correctly, setDefaultHeader sets it for all subsequent requests. Is there some other way?
myApp.service('$fileUpload', ['$http', function ($http) {
this.uploadFileToUrl = function(file, uploadUrl){
var filedata = new FormData();
filedata.append('file', file);
$http.post(uploadUrl, filedata, {
transformRequest: angular.identity,
headers: {'Content-Type': undefined}
})
.success(function(){
})
.error(function(){
});
}
}]);
You have 2 situations here, the POST for create a new item or the PUT to edit an item:
// Save new Item
$scope.saveNew = function (item) {
var data = new FormData();
angular.forEach(item, function (fieldData, field) {
data.append(field, fieldData);
});
Restangular
.all('items')
.withHttpConfig({transformRequest: angular.identity})
.post(data, {}, {'Content-Type': undefined})
.then(function () {
// do on success
}, function () {
// do on failure
});
};
// Edit existing Item
$scope.save = function (item) {
var data = new FormData();
angular.forEach(item.plain(), function (fieldData, field) {
data.append(field, fieldData);
});
Restangular
.one('items', item._id)
.withHttpConfig({transformRequest: angular.identity})
.customPUT(data, undefined, {}, {'Content-Type': undefined})
.then(function () {
$location.path('sites');
});
To set the headers for a single request all you'll need to do is add an object containing the name and value of the headers as an argument to .post(), .get() or whatever method you need.
https://github.com/mgonto/restangular#element-methods
Restangular.all('some-endpoint').post(postContent, {}, {'Content-Type': undefined}).then(function (response) {
console.log('Weeeeee!!!');
});
As for the transformRequest I am unsure of, I haven't had to deal with anything like that before, this is the only thing I could find on it in the documentation:
https://github.com/mgonto/restangular#setdefaulthttpfields
But that seems to set it for all the request which isn't what you want, but it's something at least.
Anyway, hopefully this will help you get what you want.
Edit:
Since most of the request types in restangular have a query param and then the headers you need to pass in a blank query param object and then the headers, example has been updated to show this.
Since this is the first hit on Google for this issue, see Issue 420 in the Restangular issue tracker.
Basically the newest Restangular has a withHttpConfig function to set $http options right before a request is dispatched.
If you have a route at a URL something like example.com/api/users/:id/picture that accepts a multipart upload with an image for a specific user you could do something like:
Users.one(2)
.withHttpConfig({transformRequest: angular.identity})
.customPOST(filedata, 'picture', undefined, {'Content-Type': undefined})
.then(function(resp) {
// File data post is complete here
});
By default Angular will transform any data sent with $http to JSON. The transformRequest configuration simply replaces that default transformation with a NOP.

AngularJS $http ajax request is not asynchronous and causes page to hang

I have a service where I am pulling data from server. When I click the button to send out the request to server through this service, the window freezes until I receive a response from server. Is there anything I can do to make this request asynchronous ?
Here is my service.
app.factory('service', function($http) {
return {
getLogData : function(startTime,endTime){
return $http({
url: baseURL + 'getLogData',
method: 'GET',
async: true,
cache: false,
headers: {'Accept': 'application/json', 'Pragma': 'no-cache'},
params: {'startTime': startTime , 'endTime': endTime}
});
}
};
)};
HTML.
<button ng-click="getData()">Refresh</button>
<img src="pending.gif" ng-show="dataPending" />
Code
$scope.getData = function(){
service.getLogData().success(function(data){
//process data
}).error(function(e){
//show error message
});
}
While there is some argument about the pros and cons of your approach, I am thinking that the problem is answered here: AJAX call freezes browser for a bit while it gets response and executes success
To test if this in fact part of the problem, dummy up a response and serve it statically. I use Fiddler or WireShark to get the response and then save to a file like testService.json. XHR and all of it's various derivatives like $HTTP $.ajax see it as a service though the headers might be slightly different.
Use the success promise, and wrap up the log data in a set of objects that you can attach to a $scope.
So instead of having your service have a blocking method, have it maintain a list of "LogEntries".
// constructor function
var LogEntry = function() {
/*...*/
}
var logEntries = [];
// Non-blocking fetch log data
var getLogData = function() {
return $http({
url : baseURL + 'getLogData',
method : 'GET',
async : true,
cache : false,
headers : { 'Accept' : 'application/json' , 'Pragma':'no-cache'},
params : {'startTime' : startTime , 'endTime' : endTime}
}).success(function(data) {;
// for each log entry in data, populate logEntries
// push(new LogEntry( stuff from data ))...
};
}
Then in your controller, inject your service and reference this service's log data array so Angular will watch it and change the view correctly
$scope.logEntries = mySvc.logEntries;
Then in the HTML, simply do something over logEntries:
<p ng-repeat="logEntry in logEntries">
{{logEntry}}
</p>
use this code to config
$httpProvider.useApplyAsync(true);
var url = //Your URL;
var config = {
async:true
};
var promise= $http.get(url, config);
promise.then(
function (result)
{
return result.data;
},
function (error)
{
return error;
}
);

Resources