50

I have the below request in python

import requests, json, io

cookie = {}
payload = {"Name":"abc"}
url = "/test"
file = "out/test.json"

fi = {'file': ('file', open(file) )}
r = requests.post("http://192.168.1.1:8080" + url, data=payload, files=fi, cookies=cookie)
print(r.text)

which send a file, and form fields to the backend. How can I do the same (sending file + form fields) with Angular $http. Currently, I do like this, but not sure how to send the file too.

var payload = {"Name":"abc"};
$http.post('/test', payload)
    .success(function (res) {
    //success
});
bsr
  • 57,282
  • 86
  • 216
  • 316

8 Answers8

136

I had similar problem when had to upload file and send user token info at the same time. transformRequest along with forming FormData helped:

        $http({
            method: 'POST',
            url: '/upload-file',
            headers: {
                'Content-Type': 'multipart/form-data'
            },
            data: {
                email: Utils.getUserInfo().email,
                token: Utils.getUserInfo().token,
                upload: $scope.file
            },
            transformRequest: function (data, headersGetter) {
                var formData = new FormData();
                angular.forEach(data, function (value, key) {
                    formData.append(key, value);
                });

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

                return formData;
            }
        })
        .success(function (data) {

        })
        .error(function (data, status) {

        });

For getting file $scope.file I used custom directive:

app.directive('file', function () {
    return {
        scope: {
            file: '='
        },
        link: function (scope, el, attrs) {
            el.bind('change', function (event) {
                var file = event.target.files[0];
                scope.file = file ? file : undefined;
                scope.$apply();
            });
        }
    };
});

Html:

<input type="file" file="file" required />
Pavel Netesa
  • 2,092
  • 1
  • 16
  • 11
  • 4
    I agree this should be the accepted answer because it allows you to work with your own API where tokens might be necessary – CarbonDry Aug 12 '15 at 12:13
  • 1
    Why are you adding a `Content-Type` header, then removing it in the interceptor? – Joseph Silber Oct 08 '15 at 14:55
  • 4
    the removal of `Content-Type` after being set is to make sure that when transforming the file it's form-data and after then (after being ready to do the post request) it's a normal request with `Content-Type: application/json` – Kareem Elshahawy Oct 09 '15 at 22:09
  • 9
    After much gnashing of teeth I got this to work (at least in chrome 48 and Angular 1.5.0 using .Net server side with HttpContext.Request.Files).You don't have to set the header to multipart/form-data, the XMLHttpRequest does that for you under the covers. Setting it can actually mess up the upload. use `'Content-Type': undefined` you also don't have to delete the header. This allowed me to pick up the file server side with a .Net HttpContext.Request.Files API call – KyleUp Feb 29 '16 at 03:25
  • 5
    This ALMOST works... transformRequest: angular.identity is all you need. You already have your data set in the data parameter. Set headers: { 'Content-Type': undefined } and you're done. Angular takes care of appending the boundry properly to the content type. – iGanja Apr 28 '16 at 22:51
  • Thank you for the simple directive. I've seen this almost char for char in many places, so I don't know who was the original author, but it was very helpful. – iGanja Apr 28 '16 at 22:55
  • Getting this done by using the file directive is an advisable solution. It worked for me. Thanks for the suggestion. – Yoku May 16 '17 at 21:51
  • This has issues when running with cordova. It works perfectly fine when we run it on browser but fails when we run the APK . Any solutions which worked ? – Raja May 31 '17 at 15:34
  • I have been using this solution in my AngularJS 1.3 application. I just upgraded to angular 1.6 and this no longer works - just fyi. – Shawn J. Molloy Oct 09 '17 at 22:19
  • Remember, your directive name should be unique. I've spent a lot time to solve this problem.. – aslantorret Feb 08 '18 at 17:13
  • Can any one share fiddle link? its helpful – YYY Jun 12 '18 at 09:40
26

I was unable to get Pavel's answer working as in when posting to a Web.Api application.

The issue appears to be with the deleting of the headers.

headersGetter();
delete headers['Content-Type'];

In order to ensure the browsers was allowed to default the Content-Type along with the boundary parameter, I needed to set the Content-Type to undefined. Using Pavel's example the boundary was never being set resulting in a 400 HTTP exception.

The key was to remove the code deleting the headers shown above and to set the headers content type to null manually. Thus allowing the browser to set the properties.

headers: {'Content-Type': undefined}

Here is a full example.

$scope.Submit = form => {
                $http({
                    method: 'POST',
                    url: 'api/FileTest',
                    headers: {'Content-Type': undefined},
                    data: {
                        FullName: $scope.FullName,
                        Email: $scope.Email,
                        File1: $scope.file
                    },
                    transformRequest: function (data, headersGetter) {
                        var formData = new FormData();
                        angular.forEach(data, function (value, key) {
                            formData.append(key, value);
                        });
                        return formData;
                    }
                })
                .success(function (data) {

                })
                .error(function (data, status) {

                });

                return false;
            }
CountZero
  • 6,171
  • 3
  • 46
  • 59
5

I recently wrote a directive that supports native multiple file uploads. The solution I've created relies on a service to fill the gap you've identified with the $http service. I've also included a directive, which provides an easy API for your angular module to use to post the files and data.

Example usage:

<lvl-file-upload
    auto-upload='false'
    choose-file-button-text='Choose files'
    upload-file-button-text='Upload files'
    upload-url='http://localhost:3000/files'
    max-files='10'
    max-file-size-mb='5'
    get-additional-data='getData(files)'
    on-done='done(files, data)'
    on-progress='progress(percentDone)'
    on-error='error(files, type, msg)'/>

You can find the code on github, and the documentation on my blog

It would be up to you to process the files in your web framework, but the solution I've created provides the angular interface to getting the data to your server. The angular code you need to write is to respond to the upload events

angular
    .module('app', ['lvl.directives.fileupload'])
    .controller('ctl', ['$scope', function($scope) {
        $scope.done = function(files,data} { /*do something when the upload completes*/ };
        $scope.progress = function(percentDone) { /*do something when progress is reported*/ };
        $scope.error = function(file, type, msg) { /*do something if an error occurs*/ };
        $scope.getAdditionalData = function() { /* return additional data to be posted to the server*/ };

    });
Jason
  • 15,915
  • 3
  • 48
  • 72
  • 6
    This looks to be related and some decent info, but could you perhaps explain it a bit, and in particular relate it to the code that bsr has, and how yours solves their issue? – Andrew Barber Sep 12 '13 at 16:27
3

Please, have a look on my implementation. You can wrap the following function into a service:

function(file, url) {
  var fd = new FormData();

  fd.append('file', file);

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

Please notice, that file argument is a Blob. If you have base64 version of a file - it can be easily changed to Blob like so:

fetch(base64).then(function(response) {
  return response.blob(); 
}).then(console.info).catch(console.error);
mrded
  • 4,674
  • 2
  • 34
  • 36
2

You can also upload using HTML5. You can use this AJAX uploader.

The JS code is basically:

  $scope.doPhotoUpload = function () {
    // ..
    var myUploader = new uploader(document.getElementById('file_upload_element_id'), options);
    myUploader.send();
    // ..
  }

Which reads from an HTML input element

<input id="file_upload_element_id" type="file" onchange="angular.element(this).scope().doPhotoUpload()">
Foo L
  • 10,977
  • 8
  • 40
  • 52
2

here is my solution:

// Controller
$scope.uploadImg = function( files ) {
  $scope.data.avatar = files[0];
}

$scope.update = function() {
  var formData = new FormData();
  formData.append('desc', data.desc);
  formData.append('avatar', data.avatar);
  SomeService.upload( formData );
}


// Service
upload: function( formData ) {
  var deferred = $q.defer();
  var url = "/upload" ;
  
  var request = {
    "url": url,
    "method": "POST",
    "data": formData,
    "headers": {
      'Content-Type' : undefined // important
    }
  };

  console.log(request);

  $http(request).success(function(data){
    deferred.resolve(data);
  }).error(function(error){
    deferred.reject(error);
  });
  return deferred.promise;
}


// backend use express and multer
// a part of the code
var multer = require('multer');
var storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, '../public/img')
  },
  filename: function (req, file, cb) {
    cb(null, file.fieldname + '-' + Date.now() + '.jpg');
  }
})

var upload = multer({ storage: storage })
app.post('/upload', upload.single('avatar'), function(req, res, next) {
  // do something
  console.log(req.body);
  res.send(req.body);
});
<div>
  <input type="file" accept="image/*" onchange="angular.element( this ).scope().uploadImg( this.files )">
  <textarea ng-model="data.desc" />
  <button type="button" ng-click="update()">Update</button>
</div>
邱俊霖
  • 21
  • 3
0

There are other solutions you can look into http://ngmodules.org/modules/ngUpload as discussed here file uploader integration for angularjs

Community
  • 1
  • 1
Mark Nadig
  • 4,901
  • 4
  • 37
  • 46
  • 1
    While considering those options is wise, this is not true, there are other ways to do it. See Foo L's answer or Pavel Netesa's – csga5000 Dec 04 '15 at 03:31
0

In my solution, i have

$scope.uploadVideo = function(){
    var uploadUrl = "/api/uploadEvent";


    //obj with data, that can be one input or form
    file = $scope.video;
    var fd = new FormData();


    //check file form on being
    for (var obj in file) {
        if (file[obj] || file[obj] == 0) {
            fd.append(obj, file[obj]);
        }
    }

    //open XHR request
    var xhr = new XMLHttpRequest();


    // $apply to rendering progress bar for any chunking update
    xhr.upload.onprogress = function(event) {
        $scope.uploadStatus = {
            loaded: event.loaded,
            total:  event.total
        };
        $scope.$apply();
    };

    xhr.onload = xhr.onerror = function(e) {
        if (this.status == 200 || this.status == 201) {

            //sucess

            $scope.uploadStatus = {
                loaded: 0,
                total:  0
            };


            //this is for my solution
            $scope.video = {};
            $scope.vm.model.push(JSON.parse(e.currentTarget.response));
            $scope.$apply();

        } else {
           //on else status
        }
    };

    xhr.open("POST", uploadUrl, true);

    //token for upload, thit for my solution
    xhr.setRequestHeader("Authorization", "JWT " + window.localStorage.token);


    //send
    xhr.send(fd); 
};

}