Friday, October 17, 2014

Posting files with AngularJS


While AngularJS's built-in $http service posts data as JSON by default, what do you do if you need to include an image or other binary data file in the post?

Seems pretty straightforward. Just bind HTML <input type="file"> to a controller variable via ng-model and $http.post() a FormData object containing the file object as data with Content-Type set to multipart/form-data. Right?

Not so fast. It turns out that 
  •  <input type="file"> doesn't work with ng-model, and 
  • Setting Content-Type to multipart/form-data resulted in rejected posts (a bug?).

Thankfully Angular is thoughtfully extensible. The first issue can be resolved with a custom Angular directive that tells $compile to hook <input type="file"> change events and set a controller scope variable with the selected file object when fired as so:

 .directive('inputfileModel', ['$parse', function ($parse) {  
   return {  
     link: function(scope, element, attrs) {  
       var model = $parse(attrs.inputfileModel);  
       var modelAssigner = model.assign;  
       element.bind('change', function(){  
         scope.$apply(function(){  
           modelAssigner(scope, element[0].files[0]);  
         });  
       });  
     }  
   };  
 }]);  

This directive is then set on the element as a custom element attribute:

  <input type="file" inputfile-model="fileVar">  


The second issue is a little trickier to work around. After doing some digging, it turns out that setting Content-Type to undefined results in browsers both setting  Content-Type to multipart/form-data and filling in the correct boundaries. O-Kay, not sure why that is, but hey, it works. Note that $http.post()'s default behavior of transforming data to JSON also needs to be turned off, which can be done by setting it to Angular identity function if you're into functional style programming.

 .factory('Messages', ['$http', function ($http) {  
      return {  
           put: function(url, file, dataObj, success, error) {  
                var fd = new FormData();  
                fd.append('json', JSON.stringify(dataObj));  
                fd.append('file', file);  
                $http.post(url, fd, {  
                     transformRequest: angular.identity,  
                     headers: {'Content-Type': undefined}  
                })  
                .success(success)  
                .error(error);  
           }  
      }  
 }]);  


A few problems initially, but it wasn't too hard to modify and using frameworks like Angular to add some structure to JavaScript in all its expressive glory can only help ease the pain of the poor dude down the road who will have to maintain it.