AngularJS: downloading files using $ resource (solution)

I use AngularJS to interact with a RESTful web service, using $resource to abstractly display various objects. Some of these objects are images, so I need to be able to use the save action of the $resource object to send both binary data and text fields within a single request.

How can I use the AngularJS $resource to send data and upload images to a quiet web service in a single POST request?

+48
angularjs upload angular-resource
Jan 14 '14 at 14:16
source share
3 answers

I searched around the world, and although I might have missed it, I could not find a solution to this problem: upload files using the $ resource action.

Let me take this example: our RESTful service allows us to retrieve images by accessing the /images/ endpoint. Each Image has a title, description and path pointing to the image file. Using the RESTful service, we can get all of them ( GET /images/ ), one ( GET /images/1 ) or add one ( POST /images ). Angular allows us to use the $ resource service to accomplish this task easily, but it does not allow files to be uploaded - which is required for the third action - out of the box (and they don't seem to plan to support it any time soon ). How then will we use the very convenient $ resource service if it cannot handle file downloads? Turns out it's pretty easy!

We are going to use data binding because it is one of the amazing features of AngularJS. We have the following HTML form:

 <form class="form" name="form" novalidate ng-submit="submit()"> <div class="form-group"> <input class="form-control" ng-model="newImage.title" placeholder="Title" required> </div> <div class="form-group"> <input class="form-control" ng-model="newImage.description" placeholder="Description"> </div> <div class="form-group"> <input type="file" files-model="newImage.image" required > </div> <div class="form-group clearfix"> <button class="btn btn-success pull-right" type="submit" ng-disabled="form.$invalid">Save</button> </div> </form> 

As you can see, there are two input text fields attached to each property of a single object, which I called newImage . The input file is also bound to the property of the newImage object, but this time I used a custom directive taken directly from here . This directive makes it so that every time the contents of the input file changes, the FileList object is placed inside the binded property instead of fakepath (which will be standard Angular behavior).

Our controller code is as follows:

 angular.module('clientApp') .controller('MainCtrl', function ($scope, $resource) { var Image = $resource('http://localhost:3000/images/:id', {id: "@_id"}); Image.get(function(result) { if (result.status != 'OK') throw result.status; $scope.images = result.data; }) $scope.newImage = {}; $scope.submit = function() { Image.save($scope.newImage, function(result) { if (result.status != 'OK') throw result.status; $scope.images.push(result.data); }); } }); 

(In this case, I am running the NodeJS server on my local machine on port 3000, and the response is a json object containing the status field and the optional data field).

For the file upload to work, we just need to configure the $ http service correctly, for example, in a call to the .config of the application object. In particular, we need to convert the data of each mail request into a FormData object so that it is sent to the server in the correct format:

 angular.module('clientApp', [ 'ngCookies', 'ngResource', 'ngSanitize', 'ngRoute' ]) .config(function ($httpProvider) { $httpProvider.defaults.transformRequest = function(data) { if (data === undefined) return data; var fd = new FormData(); angular.forEach(data, function(value, key) { if (value instanceof FileList) { if (value.length == 1) { fd.append(key, value[0]); } else { angular.forEach(value, function(file, index) { fd.append(key + '_' + index, file); }); } } else { fd.append(key, value); } }); return fd; } $httpProvider.defaults.headers.post['Content-Type'] = undefined; }); 

The Content-Type header is set to undefined , because setting it manually in multipart/form-data will not set a boundary value, and the server will not be able to parse the request correctly.

What is it. Now you can use the $resource to save() objects containing both standard data fields and files.

WARNING This has some limitations:

  • It does not work in older browsers. Sorry: (
  • If your model has β€œembedded” documents, for example

    { title: "A title", attributes: { fancy: true, colored: false, nsfw: true }, image: null }

    then you need to reorganize the transformRequest function accordingly. You could, for example, JSON.stringify nested objects, if you can JSON.stringify them at the other end

  • English is not my main language, so if my explanation is unclear, tell me and I will try to rephrase it :)

  • This is just an example. You can expand it, depending on what you need to do.

Hope this helps, cheers!

EDIT:

As @david pointed out , a less invasive solution would be to define this behavior only for those $resource that really need it, and not for converting each and every request made by AngularJS. You can do this by creating $resource as follows:

 $resource('http://localhost:3000/images/:id', {id: "@_id"}, { save: { method: 'POST', transformRequest: '<THE TRANSFORMATION METHOD DEFINED ABOVE>', headers: '<SEE BELOW>' } }); 

As for the title, you should create one that suits your requirements. The only thing you need to specify is the 'Content-Type' property, setting it to undefined .

+45
Jan 14 '14 at 14:16
source share

The smallest and least invasive solution for sending $resource requests using FormData I found the following:

 angular.module('app', [ 'ngResource' ]) .factory('Post', function ($resource) { return $resource('api/post/:id', { id: "@id" }, { create: { method: "POST", transformRequest: angular.identity, headers: { 'Content-Type': undefined } } }); }) .controller('PostCtrl', function (Post) { var self = this; this.createPost = function (data) { var fd = new FormData(); for (var key in data) { fd.append(key, data[key]); } Post.create({}, fd).$promise.then(function (res) { self.newPost = res; }).catch(function (err) { self.newPostError = true; throw err; }); }; }); 
+18
Oct 26 '15 at 16:34
source share

Please note that this method will not work with 1.4.0+. For more information check AngelJS changelog (search $http: due to 5da1256 ) and this question . This was actually an unintended (and therefore remote) behavior on AngularJS.

I came up with this function to convert (or add) form data to a FormData object. It could be used as a service.

The logic below should be inside the transformRequest configuration or inside $httpProvider or it can be used as a service. In any case, the Content-Type header must be NULL, and it depends on the context into which you insert this logic. For example, inside the transformRequest parameter when setting up a resource, you do:

 var headers = headersGetter(); headers['Content-Type'] = undefined; 

or when setting up $httpProvider you can use the method specified in the answer above.

In the example below, the logic is placed inside the transformRequest method for the resource.

 appServices.factory('SomeResource', ['$resource', function($resource) { return $resource('some_resource/:id', null, { 'save': { method: 'POST', transformRequest: function(data, headersGetter) { // Here we set the Content-Type header to null. var headers = headersGetter(); headers['Content-Type'] = undefined; // And here begins the logic which could be used somewhere else // as noted above. if (data == undefined) { return data; } var fd = new FormData(); var createKey = function(_keys_, currentKey) { var keys = angular.copy(_keys_); keys.push(currentKey); formKey = keys.shift() if (keys.length) { formKey += "[" + keys.join("][") + "]" } return formKey; } var addToFd = function(object, keys) { angular.forEach(object, function(value, key) { var formKey = createKey(keys, key); if (value instanceof File) { fd.append(formKey, value); } else if (value instanceof FileList) { if (value.length == 1) { fd.append(formKey, value[0]); } else { angular.forEach(value, function(file, index) { fd.append(formKey + '[' + index + ']', file); }); } } else if (value && (typeof value == 'object' || typeof value == 'array')) { var _keys = angular.copy(keys); _keys.push(key) addToFd(value, _keys); } else { fd.append(formKey, value); } }); } addToFd(data, []); return fd; } } }) }]); 

Thus, you can do the following without problems:

 var data = { foo: "Bar", foobar: { baz: true }, fooFile: someFile // instance of File or FileList } SomeResource.save(data); 
+8
Sep 11 '14 at 19:37
source share



All Articles