144

I want to do a simple multipart form post from AngularJS to a node.js server, the form should contain a JSON object in one part and an image in the other part, (I'm currently posting only the JSON object with $resource)

I figured I should start with input type="file", but then found out that AngularJS can't bind to that..

all the examples I can find are for wraping jQuery plugins for drag & drop. I want a simple upload of one file.

I'm new to AngularJS and don't feel comfortable at all with writing my own directives.

georgeawg
  • 46,994
  • 13
  • 63
  • 85
Gal Ben-Haim
  • 16,546
  • 19
  • 71
  • 127
  • 1
    i think this might help: http://noypi-linux.blogspot.com/2013/04/form-fileuploader-in-purely-angularjs.html – Noypi Gilas Jul 10 '14 at 09:49
  • 1
    See this answer: http://stackoverflow.com/questions/18571001/file-upload-using-angularjs/20506037#20506037 Plenty of options there for already working systems. – Anoyz Nov 06 '14 at 15:49
  • 1
    [See here](https://angular-file-upload.appspot.com/) – bobobobo Dec 27 '14 at 22:53

7 Answers7

188

A real working solution with no other dependencies than angularjs (tested with v.1.0.6)

html

<input type="file" name="file" onchange="angular.element(this).scope().uploadFile(this.files)"/>

Angularjs (1.0.6) not support ng-model on "input-file" tags so you have to do it in a "native-way" that pass the all (eventually) selected files from the user.

controller

$scope.uploadFile = function(files) {
    var fd = new FormData();
    //Take the first selected file
    fd.append("file", files[0]);

    $http.post(uploadUrl, fd, {
        withCredentials: true,
        headers: {'Content-Type': undefined },
        transformRequest: angular.identity
    }).success( ...all right!... ).error( ..damn!... );

};

The cool part is the undefined content-type and the transformRequest: angular.identity that give at the $http the ability to choose the right "content-type" and manage the boundary needed when handling multipart data.

Fabio Bonfante
  • 4,920
  • 1
  • 28
  • 35
  • That's very cool, any idea how to implement an IE fallback(no support for FormData object), or how to include other form fields within that multipart? – Oleg Belousov Oct 29 '13 at 08:15
  • I've another "native js" implementation without using $http, but it always requires the FormData object... if IE is missing this (really?!! forms exists since HTML1.0!!!) maybe the only way is to "mock" the object... trye take a look to http://modernizr.com/ – Fabio Bonfante Oct 29 '13 at 10:33
  • 2
    @Fabio Can u plz explain me what does this transformRequest do ? what the angular.identity is ? I was banging my head through out the day just to accomplish multipart file upload. – agpt Feb 21 '14 at 16:39
  • When I do this the success callback is invoked before the multipart upload actually completes so I only get the 'provisional' response and headers. Crucially the `Location` header returned by S3 that contains the URL for the uploaded file is not present at the time the success callback is invoked. – Rob Fletcher Feb 27 '14 at 16:26
  • @RobFletcher is this happening with version 1.0.6 or newer? any news on interoperating with S3? thanks in advance. – Fabio Bonfante Mar 18 '14 at 08:17
  • Using something like angular.element(this).scope().uploadFile(this.files) is far far away from any best practice in angular. – Diego Plentz Jun 03 '14 at 18:33
  • Hi @DiegoPlentz, sadly not support ng-change in all angular 1.x versions. The reason argued is to provide a common behaviour in all supported browsers (angular 1.x has IE9 as baseline). This will probably change in version 2.x. https://github.com/angular/angular.js/issues/1375 – Fabio Bonfante Jun 05 '14 at 07:20
  • @Fabio how do I get the `fd` in backend (action method) and how actually store the fileData? – RandomUser Sep 12 '14 at 06:23
  • 1
    @RandomUser in a java rest application, something like that http://www.mkyong.com/webservices/jax-rs/file-upload-example-in-jersey/ – Fabio Bonfante Sep 12 '14 at 20:59
  • 2
    wow, just brilliant, thanks a lot. Here I have to upload multiple images and some other data as well so i manipulate fd as `fd.append("file", files[0]); fd.append("file",files[1]); fd.append("name", "MyName")` – Moshii Oct 18 '14 at 12:04
  • I really don't understand why this works, but this works... The `transformRequest: angular.identity` part seems a bit magic. – Martin Apr 29 '15 at 15:52
  • 1
    console.log(fd) shows form is empty? is it that way? – J Bourne Jul 21 '15 at 11:46
  • I don't know why don't accept this solution. Thanks – Bilal BBB Jul 23 '15 at 19:53
  • and now, how do i retrieve my file on server? – Pbd Aug 18 '15 at 11:33
  • @Pbd in a java rest application, something like that mkyong.com/webservices/jax-rs/file-upload-example-in-jersey – Fabio Bonfante Aug 19 '15 at 20:48
  • Will this code save the uploaded image in our app folder?? – answer99 Aug 25 '15 at 11:17
  • 4
    Note that this won't work if you have [debugInfo](https://docs.angularjs.org/guide/production#disabling-debug-data) disabled (as recommended) – Bruno Peres Sep 08 '15 at 03:29
  • @BrunoPeres thanks for this information, it's specific to an angular version? You know if already exists an opened bug on this? – Fabio Bonfante Sep 08 '15 at 19:53
  • 1
    @Fabio It's not really a bug, the `$compileProvider.debugInfoEnabled()` (available since Angular 1.3) unable scopes from being accessable from outside of angular contexts (which is done by the answer, on the `onchange` attribute). Disabling the the debugInfo is a recommendation to run angular on production environments, but is enabled by default. So you won't have any issues with it, you will just be unable to disable the debugInfo if you upload images using this approach. – Bruno Peres Sep 09 '15 at 18:13
  • This is genius. – Leon Feb 07 '17 at 14:32
  • The multipart is sent without the content of the file. Any Idea why? – Gilo Jun 03 '17 at 18:29
  • 1
    @gilo I know that with chrome, the developer tools, doesn't show the content-file for multipart request. Give it a try with firefox... or if you want be really sure try with wireshark. – Fabio Bonfante Jun 05 '17 at 17:11
  • Thank you mate, you made me smile again after 3 days of trying to work around until I came across to your elegant pure AngularJS solution! – HelloIT Jun 04 '19 at 19:40
43

You can use the simple/lightweight ng-file-upload directive. It supports drag&drop, file progress and file upload for non-HTML5 browsers with FileAPI flash shim

<div ng-controller="MyCtrl">
  <input type="file" ngf-select="onFileSelect($files)" multiple>
</div>

JS:

//inject angular file upload directive.
angular.module('myApp', ['ngFileUpload']);

var MyCtrl = [ '$scope', 'Upload', function($scope, Upload) {
  $scope.onFileSelect = function($files) {
  Upload.upload({
    url: 'my/upload/url',
    file: $files,            
  }).progress(function(e) {
  }).then(function(data, status, headers, config) {
    // file is uploaded successfully
    console.log(data);
  }); 

}];
danial
  • 3,853
  • 2
  • 28
  • 38
  • Isn't this performing a single POST request for each file at a time? – Anoyz Dec 12 '13 at 18:27
  • Yes it does. There are discussions in the github issue tracker about why it is better to upload the files one by one. The Flash API doesn't support sending files together and AFAIK Amazon S3 does not support it either. – danial Dec 12 '13 at 22:47
  • So, you're saying the the general more correct approach is sending one file POST request at a time? I can see several advantages in that, but also more trouble when making server-side restful support. – Anoyz Dec 13 '13 at 10:44
  • 2
    The way I implement it is to upload each file as a resource save and the server will save it on the local file system (or database) and return a unique id (i.e. random folder/file name or db id) for that file. Then once all uploads are done, client sends another PUT/POST request which extra data and ids of the files that are uploaded for this request. Then the server would save the record with associated files. It is like gmail when you upload files and then send the email. – danial Dec 13 '13 at 14:46
  • Version 1.1.11 has the support for multiple file upload in one request but it will only work for HTML5 FormData browsers. – danial Dec 14 '13 at 01:23
  • That's great! The thing is, for prototyping it's much easier to have a single POST strategy. I agree the best way is sending multiple POST requests though. – Anoyz Dec 14 '13 at 16:58
  • $upload is deprecated in latest version of angular-file-upload . Hope we should change it for latest version of angular-file-upload – Zeel Shah Jul 06 '15 at 15:09
  • how to do this when we are not using input field, we are using a capture plugin which is giving file path – ashishkumar148 Aug 24 '15 at 16:27
  • For browser's security you cannot set the file path of the input type file. If the plugin gives you the data url or binary of the file you can upload it using this plugin, just convert it to a blob object and pass it as `file` option. – danial Aug 24 '15 at 20:58
  • I have used this and it worked like a charm. But now I want to implement the form validations and make it required. Can anyone help me with this ? I took reference from the following SO question : http://stackoverflow.com/questions/16207202/required-attribute-not-working-with-file-input-in-angular-js – Vaibhav Pachauri Nov 03 '15 at 07:49
  • 1
    This is not "simple/lightweight". The samples section doesn't even have an example of uploading just one file. – Chris Jul 06 '16 at 16:06
  • The first sample is a form submit for uploading one file. – danial Jul 18 '16 at 09:00
  • Is the accepted answers explanation about `ng-model` on input of type file the reason this source code you link creates an `` on the DOM (if it doesn't already exist)? How peculiar. – Summer Developer Dec 01 '16 at 00:54
9

It is more efficient to send a file directly.

The base64 encoding of Content-Type: multipart/form-data adds an extra 33% overhead. If the server supports it, it is more efficient to send the files directly:

$scope.upload = function(url, file) {
    var config = { headers: { 'Content-Type': undefined },
                   transformResponse: angular.identity
                 };
    return $http.post(url, file, config);
};

When sending a POST with a File object, it is important to set 'Content-Type': undefined. The XHR send method will then detect the File object and automatically set the content type.

To send multiple files, see Doing Multiple $http.post Requests Directly from a FileList


I figured I should start with input type="file", but then found out that AngularJS can't bind to that..

The <input type=file> element does not by default work with the ng-model directive. It needs a custom directive:

Working Demo of "select-ng-files" Directive that Works with ng-model1

angular.module("app",[]);

angular.module("app").directive("selectNgFiles", function() {
  return {
    require: "ngModel",
    link: function postLink(scope,elem,attrs,ngModel) {
      elem.on("change", function(e) {
        var files = elem[0].files;
        ngModel.$setViewValue(files);
      })
    }
  }
});
<script src="//unpkg.com/angular/angular.js"></script>
  <body ng-app="app">
    <h1>AngularJS Input `type=file` Demo</h1>
    
    <input type="file" select-ng-files ng-model="fileArray" multiple>
    
    <h2>Files</h2>
    <div ng-repeat="file in fileArray">
      {{file.name}}
    </div>
  </body>

$http.post with content type multipart/form-data

If one must send multipart/form-data:

<form role="form" enctype="multipart/form-data" name="myForm">
    <input type="text"  ng-model="fdata.UserName">
    <input type="text"  ng-model="fdata.FirstName">
    <input type="file"  select-ng-files ng-model="filesArray" multiple>
    <button type="submit" ng-click="upload()">save</button>
</form>
$scope.upload = function() {
    var fd = new FormData();
    fd.append("data", angular.toJson($scope.fdata));
    for (i=0; i<$scope.filesArray.length; i++) {
        fd.append("file"+i, $scope.filesArray[i]);
    };

    var config = { headers: {'Content-Type': undefined},
                   transformRequest: angular.identity
                 }
    return $http.post(url, fd, config);
};

When sending a POST with the FormData API, it is important to set 'Content-Type': undefined. The XHR send method will then detect the FormData object and automatically set the content type header to multipart/form-data with the proper boundary.

Community
  • 1
  • 1
georgeawg
  • 46,994
  • 13
  • 63
  • 85
  • The `filesInput` directive doesn't appear to be working with Angular 1.6.7, I can see the files in the `ng-repeat` but the same `$scope.variable` is blank in the controller? Also one of your example uses `file-model` and one `files-input` – Dan Aug 28 '18 at 18:52
  • @Dan I tested it and it works. If you are having a problem with your code, you should ask a new question with a [Minimal, Complete, Verifiable example](https://stackoverflow.com/help/mcve). Changed the directive name to `select-ng-files`. Tested with AngularJS 1.7.2. – georgeawg Aug 29 '18 at 02:56
5

I just had this issue. So there are a few approaches. The first is that new browsers support the

var formData = new FormData();

Follow this link to a blog with info about how support is limited to modern browsers but otherwise it totally solves this issue.

Otherwise you can post the form to an iframe using the target attribute. When you post the form be sure to set the target to an iframe with its display property set to none. The target is the name of the iframe. (Just so you know.)

I hope this helps

Community
  • 1
  • 1
Subtubes
  • 12,859
  • 21
  • 59
  • 90
  • AFAIK FormData doesn't work with IE. maybe its a better idea to do base64 encoding of the image file and send it in JSON? how can I bind to a input type="file" with AngularJS to get the chosen file path ? – Gal Ben-Haim Dec 20 '12 at 08:33
3

I know this is a late entry but I have created a simple upload directive. Which you can get working in no time!

<input type="file" multiple ng-simple-upload web-api-url="/api/post"
       callback-fn="myCallback" />

ng-simple-upload more on Github with an example using Web API.

georgeawg
  • 46,994
  • 13
  • 63
  • 85
shammelburg
  • 5,386
  • 7
  • 24
  • 34
  • 2
    to be honest, suggesting to copy-paste code in your project readme can be a big black mark. try integrating your project with common package managers like npm or bower. – Stefano Torresi Mar 07 '16 at 15:09
2

You could upload via $resource by assigning data to params attribute of resource actions like so:

$scope.uploadFile = function(files) {
    var fdata = new FormData();
    fdata.append("file", files[0]);

    $resource('api/post/:id', { id: "@id" }, {
        postWithFile: {
            method: "POST",
            data: fdata,
            transformRequest: angular.identity,
            headers: { 'Content-Type': undefined }
        }
    }).postWithFile(fdata).$promise.then(function(response){
         //successful 
    },function(error){
        //error
    });
};
georgeawg
  • 46,994
  • 13
  • 63
  • 85
Emeka Mbah
  • 13,839
  • 6
  • 61
  • 84
1

I just wrote a simple directive (from existing one ofcourse) for a simple uploader in AngularJs.

(The exact jQuery uploader plugin is https://github.com/blueimp/jQuery-File-Upload)

A Simple Uploader using AngularJs (with CORS Implementation)

(Though the server side is for PHP, you can simple change it node also)

Community
  • 1
  • 1
sk8terboi87 ツ
  • 3,119
  • 2
  • 31
  • 44