Update 16 July 2015: I’ve updated the JSFiddle demo with a more expressive log statement. So glad this post has been and continues to prove helpful!

I was writing my first AngularJS app and wanted to create a reusable service that I could call anywhere in the controller to upload the contents of a file input element in the view. A couple issues immediately arose: Angular’s ng-model doesn’t work on inputs with type=“file” so we need to create our own directive to bind files to variables in the view controller. Once we have the file, we need to override some Angular defaults and send it with a multipart/form-data request. The service in this post accomplishes this for us.

Getting the file in the controller’s scope

First we write the directive to gain access to the file object in our controller. In the snippet below, file-model is an attribute on a file input element, and its value is the name of the variable in our controller’s scope that binds to the file object.

.directive('fileModel', ['$parse', function ($parse) {
    return {
        restrict: 'A',
        link: function(scope, element, attrs) {
            var model = $parse(attrs.fileModel);
            var modelSetter = model.assign;

            element.bind('change', function(){
                scope.$apply(function(){
                    modelSetter(scope, element[0].files[0]);
                });
            });
        }
    };
}]);

In the link function of the above directive, we listen for changes to the content of the input element and change the value of the variable in the scope accordingly. This is achieved using the $parse service to set the value in our scope. The markup required in the view for the directive to work is simple:

<input type="file" file-model="myFile"/>

Making the multipart/form-data request

Now we’re able to access the file in the view controller with $scope.myFile. In order to upload the file to the server with a multipart/form-data request, we pass the file object and the url to a service and override some of Angular’s default behavior. Note that the below service uses the FormData object which is not supported by IE9 and earlier.

.service('fileUpload', ['$http', function ($http) {
    this.uploadFileToUrl = function(file, uploadUrl){
        var fd = new FormData();
        fd.append('file', file);
        $http.post(uploadUrl, fd, {
            transformRequest: angular.identity,
            headers: {'Content-Type': undefined}
        })
        .success(function(){
        })
        .error(function(){
        });
    }
}]);

Angular’s default transformRequest function will try to serialize our FormData object, so we override it with the identity function to leave the data intact. Angular’s default Content-Type header for POST and PUT requests is application/json, so we want to change this, too. By setting ‘Content-Type’: undefined, the browser sets the Content-Type to multipart/form-data for us and fills in the correct boundary. Manually setting ‘Content-Type’: multipart/form-data will fail to fill in the boundary parameter of the request.

In my use case, I did not want to upload the file until I knew a previous request went through successfully, so I inject the service into my controller and call it when I know it’s safe to upload.

var file = $scope.myFile;
var uploadUrl = 'http://www.example.com/images';
fileUpload.uploadFileToUrl(file, uploadUrl);

Check out the JSFiddle

To see the view, controller, directive, and service working together in their entirety, check out the small fiddle I made. In the fiddle, the file upload function is invoked on the click of the button, but of course it can be called anywhere in your controller.

Share Via

View Comments