AngularJS File Upload

May 3, 2013

IntroductionAngularJS

Recently I had some time to play around with AngularJS. More specifically I had to implement a jQuery based file upload widget.

The widget’s demo site already contains an AngularJS demo, but I wanted a minimum setup, so I started from scratch and figured out the necessary parts to implement the file upload using ASP.NET MVC as the server-side platform.

Let’s see which steps we need to take to implement a basic version.

Table Of Contents

Server Side

Let’s focus on the server-side part first.

public class HomeController : Controller
{
    public ActionResult Index()
    {
        return View();
    }
}

Can’t get much simpler.

Now you’ll just need to add one more action method which handles a file upload.

[HttpPost]
public ContentResult Upload(HttpPostedFileBase file)
{
    var filename = Path.GetFileName(file.FileName);
    var path = Path.Combine(Server.MapPath("~/uploads"), filename);
    file.SaveAs(path);

    return new ContentResult
    {
        ContentType = "text/plain",
        Content = filename,
        ContentEncoding = Encoding.UTF8
    }; 
}

The code is self-explanatory. When a file is uploaded (POST) the data is bound to the file parameter which is of type HttpPostedFileBase. The file name is extracted and the file is saved on the server. Finally we return the file name as plain text.

Voila, that’s all the server side code you’ll need.

Remark: I had to explicitly set the content type otherwhise IE will prompt you to download the result. Feel free to change the content type (e.g. JSON) and return something else than the filename instead.

Top of page

Dependencies

When setting up a small demo site I developed the tendency to implement Twitter Bootstrap so that I can easily style it. If you are not familiar with Bootstrap, check out the following article to get started:

Getting started with Twitter Bootstrap

So if you see some unfamiliar CSS in the HTML listings you know where it’s coming from.

Apart from Bootstrap and the jQuery file upload widget you’ll need to include the following resources:

The download package of the jQuery file upload contains a couple of JS files, but you’ll only need the jquery.fileupload.js library.

Top of page

Module

Let’s create a custom Angular module. Add a new script file called app.js to the project and add the following code to it:

(function () {
    'use strict';

    var myApp = angular.module('myApp', []);
}());

Don’t forget to bootstrap the application.

<html ng-app="myApp">
...
</html>

The root of our Angular application is now defined, let’s flesh out the rest.

Top of page

Layout

The file upload control looks as follows:

fileupload

Let’s compose the HTML to create this layout. First add a DIV and link it an Angular controller named FileUploadCtrl. Ignore the controller for now, we’ll get back to it later.

<h2>File Upload</h2>
<div ng-controller="FileUploadCtrl">
...
</div>

The remaining HTML can be split into three sections:

  • Select (files) button
  • List of selected files
  • Upload button

Start by adding the HTML for the select button.

<div class="control-group">
    <div class="controls">
        <span class="btn btn-success fileinput-button">
            <i class="icon-plus icon-white"></i>
            <input type="file" name="file" data-url="home/upload" 
                   multiple upload><span>Add files...</span>
        </span>
    </div>
</div>

Remark: You’ll need to include some additional CSS to properly style the file upload input, because by default it is designed to be butt-ugly (I borrowed this CSS from the file upload demo page).

Next add a list to display the selected files names. The list is bound to an array (files) of our Angular controller.

<div>
    <span ng-show="!files.length">No files selected</span>
    <ul>
        <li ng-repeat="file in files">{{file}}</li>
    </ul>
</div>

Last, but not least: the Upload button.

<div class="form-actions">
    <button type="submit" class="btn btn-primary pull-left" ng-click="upload()">Upload</button>
</div>

Clicking the button will invoke the upload function on the controller’s scope.

Top of page

FileUploadCtrl

Time to add our FileUploadCtrl to our Angular module.

myApp.controller('FileUploadCtrl',
    ['$scope', '$rootScope', 'uploadManager', 
    function ($scope, $rootScope, uploadManager) {
    $scope.files = [];
    $scope.percentage = 0;

    $scope.upload = function () {
        uploadManager.upload();
        $scope.files = [];
    };

    $rootScope.$on('fileAdded', function (e, call) {
        $scope.files.push(call);
        $scope.$apply();
    });

    $rootScope.$on('uploadProgress', function (e, call) {
        $scope.percentage = call;
        $scope.$apply();
    });
}]);

We inject 3 dependencies into the controller: $scope, $rootScope and uploadManager. The latter being a custom service which manages the files that we want to upload. The scope has two properties, files (array) and percentage (int). The files array contains the name of the files to be uploaded. We’ll get back to the percentage later.

When you click the upload button the upload function on the scope is called. It informs the uploadManager service to start uploading the files and resets the files array.

Via the $rootScope we also listen to two events, fileAdded and uploadProgress, which are broadcast by the uploadManager service. Each time a file is added using the third-party jQuery plugin the fileAdded event is triggered. When this happens we add the filename to the files array. The uploadProgress event is triggered when the widget is busy uploading the files. Here we update the percentage property. Later we’ll bind this to a progress bar.

Top of page

uploadManager service

The uploadManager service is fairly straightforward. It manages the files you wish to upload, it allows you to communicate between the third party file upload widget and your Angular controller.

Only the $rootScope is injected as a dependency. We use it to broadcast the two events (fileAdded and uploadProgress) we mentioned earlier.

myApp.factory('uploadManager', function ($rootScope) {
    var _files = [];
    return {
        add: function (file) {
            _files.push(file);
            $rootScope.$broadcast('fileAdded', file.files[0].name);
        },
        clear: function () {
            _files = [];
        },
        files: function () {
            var fileNames = [];
            $.each(_files, function (index, file) {
                fileNames.push(file.files[0].name);
            });
            return fileNames;
        },
        upload: function () {
            $.each(_files, function (index, file) {
                file.submit();
            });
            this.clear();
        },
        setProgress: function (percentage) {
            $rootScope.$broadcast('uploadProgress', percentage);
        }
    };
});

Top of page

upload Directive

So the uploadManager service triggers events to which our controller listens and consequently updates its scope. But who then notifies this service of events triggered by the file upload widget?

That’s where our custom upload directive comes into play. Take a look again at the input element for the file upload listed earlier.

 <input type="file" name="file" data-url="home/upload"
        multiple upload><span>Add files...</span>

The data-url attribute points to the URL the files are POSTed to. Note the upload attribute. This extends the input element. Let’s discover what it does.

myApp.directive('upload', ['uploadManager', function factory(uploadManager) {
    return {
        restrict: 'A',
        link: function (scope, element, attrs) {
            $(element).fileupload({
                dataType: 'text',
                add: function (e, data) {
                    uploadManager.add(data);
                },
                progressall: function (e, data) {
                    var progress = parseInt(data.loaded / data.total * 100, 10);
                    uploadManager.setProgress(progress);
                },
                done: function (e, data) {
                    uploadManager.setProgress(0);
                }
            });
        }
    };
}]);

First we have Angular inject the uploadManager service. We then use the link function to transform the DOM. Here our element is turned into a file upload widget. The widget supports a number of callbacks. Here we only use the add, progressall and done callbacks. When these are triggered by the widget we notify the uploadManager. For example when a file is added we pass this to the uploadManager which then broadcasts the fileAdded event. Since our controller is listening for this event, it’ll be notified when a new file has been selected.

Top of page

Progress bar

Let’s get back to that progress property on the scope of our controller. Each time the uploadManager service broadcasts the uploadProgress event we update the percentage.

This way you can easily add a progress bar and show the upload progress. For example:

<div class="progress" ng-show="percentage">
    <div class="bar" style="width: {{percentage}}%;"></div>
</div>

Top of page

Summary

To recapitulate, we have 3 components that make up our Angular application:

  • FileUploadCtrl: Our controller which is tied to a DIV which contains the file upload widget
  • uploadManager Service: This service manages the files we want to upload. It sends out a couple of events to which our controller listens and acts to accordingly.
  • upload Directive: This directive transforms the file input into a bonafide upload widget. It uses the widget’s callbacks to notify the uploadManager service.

And that’s basically all there is too it. I tested the code in Chrome and IE8. If you have any issues or suggestions please let me know.

Top of page

22 Responses to “AngularJS File Upload”

  1. Paullus Nava Says:

    Hey, there… thanks a lot for sharing your acknowledgment…

    I used your code, but I needed to get Header HTTP response to get filename that was recorded on server side. Five hours after a lot of hacking I finally got it…

    Even thoughtr it’s far from perfect (HTTP header name is hard coded) but if you want to I can send you the code that I changed so that you can help others that have the same need as I had.

    Let me know if you want to.

  2. Mark Says:

    Thanks for a terrific walkthrough. I’m getting a null HttpPostedFileBase in the controller. Made sure the controller method parameter name matches the input name. Just somehow missing on the model binding, apparently.

  3. Angel Says:

    hey dude. your directive is really good. i would like to know how you can update the item that is added to the array of the files

    for example in the ploadManager.add(data); you push the file in the queue of files, but what if you want to update this file once the done function is executed for add the new path of your file uploaded.

    since you already add your file to the array how you will be able to update? i was think about use something like files[file.length].filepath = ‘/url/path/file.jpg’;
    but seems doesn’t work.
    did you did something like that to preview the image once it’s already upload on your server ?

    Thank you in advance

  4. Vitor Says:

    Could you upload the full source code to this somewhere?
    I’m having some problems getting this to run properly in my app (because I’m just getting started with angular.js).

  5. Janak Says:

    Yes, please upload full source code, please!!!!

  6. Max Says:

    Thanks a lot for the article! it’s straight, understandable and everything worked from the first launch without any additional magic :)

  7. Ricardo Says:

    Hello,
    Based on your code I need to use file upload Post with something dynamic, using angularjs databinding fail, let me show you:

    Plantilla


    Add files…

    So, as you see I’m trying to use databinding into data-url ({{selectedTemplate.id}}), but this fail, and Post to “”home/upload?uploadId={{selectedTemplate.id}}” literally. The ID changes based on the seleted item, so I need to do this Post dynamically.
    How can I do this?

    • Lars Johannesson Says:

      Hey, I’m having the exact same problem (trying to use angular binding in the data-url attribute). Did you manage to solve it?

  8. Angel Says:

    hi recardo guess i can help you with, let me see your code to try to give you a hand…


  9. Can you upload the source code to the whole project.

  10. Danial Says:

    I have put together a simple/light angular directive with polyfill for browsers not supporting HTML5 FormData here :
    https://github.com/danialfarid/angular-file-upload

  11. John Says:

    Thanks bro, cool stuff

  12. grylls Says:

    I watched your code, but my project does not allow to use $rootScope. So what can we listen the add method in uploadManager.

  13. Vlad Says:

    Hi Christophe.
    Thank you for the article.
    I’m trying to getting it work in IE9. It doesn’t do a post as a multipart form and each time I get http 415 (media not supported) error. It works well in FF and Chrome however. Can you please advice. Thanks a lot.

  14. miliu99 Says:

    Thanks for the article. One question: How do you pass additional parameters to Upload function on server? For example, public ContentResult Upload(HttpPostedFileBase file, string id);

  15. Silvio Silva Says:

    Very good blog post, thanks for sharing.

  16. Raja Rajendraprasath Says:

    can you please provide full code


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 341 other followers

%d bloggers like this: