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 .