Code

A directive to manage file upload in an AngularJS application

, 19 Comments

This week I was in the need to add the functionality of file upload to an AngularJS web application, on which I’m working for a personal project. Since my project also uses jQuery, I found that the better solution to build an asynchronous file uploader was to rely on the well known (and wonderful) jQuery plugin jquery.form, created by M. Alsup.

At the end I wrote an AngularJS directive to have a component, that I can reuse in my next projects. I will share it in this post.
Before digging into the code you can take a look at the live demo.

Getting Started

In order to getting started to use the directive, I had to include in the project jQuery, and the jquery.form plugin. There are no further preliminary operations.


<script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/2.0.2/jquery.js"></script>
<script type="text/javascript" src="jquery.form.js"></script>

Cool, let’s procede to the code of the directive.

The uploader directive

Lately, I really like create new HTML elements… and since I’m not really fantasious, I simply called the directive to upload file: uploader. It’s possible to use it almost everywhere, but in a form. It’s easy as:


<form enctype="multipart/form-data">
	<uploader action="uploader.php"></uploader>
</form>

Maybe you can ask why I setted the action attribute on the uploader element, instead that on the form. This is due to the fact that, when an action attribute is defined for a form, by default AngularJS reloads the page when the form is submitted. I really don’t want this behaviour (and nobody wants it more in 2013), so I will use the action attribute just for the time in which I will really need it.

Inside the uploader

It’s time to look inside the uploader element.
I defined its body into a separated HTML file (that with an effort of fantasy I called uploader.html), that then I will reference as template from the directive definition object. This is its content:


<!-- uploader.html template -->
<div class="uploader">

	<!-- the real input[type=file] is hidden -->
	<input type="file" name="uploader" style="display:none;"
		 onchange="angular.element(this).scope().sendFile(this);"/>

	<!-- input field, used to open the real input[type=file]  -->
	<div class="fake-uploader">
		<label for="uploader"></label>
		<input type="text" readonly="readonly" ng-model="avatar"/>
	</div>

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

</div>

<!-- preview -->
<div ng-if="avatar != ''" style="margin: 50px 0 0;">
	<img ng-src="upload/{{avatar}}"/>
</div>

Two things to note here; the first is that I’m hiding the real file input element (I really don’t like how it looks)… I replaced it with a classic text input file, in readonly mode, that I will also use to trigger a click on the hidden input file.
The second thing is the way I binded the change event on the input file:


// retrive the scope of 'element'
angular.element(element).scope();

Since I used the old classic onchange attribute, I need to get the current scope… once this is done, it’s possible to use all the method defined in the current scope.

The directive definition

Finally, it’s time to give a sight to the directive’s definition. To mantain the code as compact as possible, I will start showing the backbone of the directive, then I will go deeper to each section.


var dir = angular.module('app.directives', []);
dir.directive('uploader', [function() {

	return {
		restrict: 'E',
		scope: {
			
			// scope
			// define a new isolate scope

		},
		controller: ['$scope', function ($scope) {

			// controller:
			// here you should define properties and methods
			// used in the directive's scope

		}],
		link: function(scope, elem, attrs, ctrl) {
			
			// link function 
			// here you should register listeners
		
		},
		replace: false,    
		templateUrl: 'uploader.html'
	};

}]);

I used the scope property to create a new isolated scope for the directive. In this isolated scope I defined the action property, that takes its value from the DOM, exactly from the action attribute, that I previously setted on the uploader element.


scope: {
	action: '@'
}

If you can’t understand this step, you’ll find useful the AngularJS doc about the directive definition object.
As I anticipated, since the input file is hidden, I need to trigger a click on it via javascript… the link function is the right place where to place the event listeners:


link: function(scope, elem, attrs, ctrl) {
	elem.find('.fake-uploader').click(function() {
		elem.find('input[type="file"]').click();
	});
}

Finally, in the controller property, I defined the sendFile method, that wraps the ajaxSubmit api of the jquery.form plugin.


controller: ['$scope', function ($scope) {

	$scope.progress = 0;
	$scope.avatar = '';

	$scope.sendFile = function(el) {

		var $form = $(el).parents('form');

		if ($(el).val() == '') {
			return false;
		}

		$form.attr('action', $scope.action);

		$scope.$apply(function() {
			$scope.progress = 0;
		});				

		$form.ajaxSubmit({
			type: 'POST',
			uploadProgress: function(event, position, total, percentComplete) { 
				
				$scope.$apply(function() {
					// upload the progress bar during the upload
					$scope.progress = percentComplete;
				});

			},
			error: function(event, statusText, responseText, form) { 

				// remove the action attribute from the form
				$form.removeAttr('action');

				/*
					handle the error ...
				*/

			},
			success: function(responseText, statusText, xhr, form) { 

				var ar = $(el).val().split('\\'), 
					filename =  ar[ar.length-1];

				// remove the action attribute from the form
				$form.removeAttr('action');

				$scope.$apply(function() {
					$scope.avatar = filename;
				});

			},
		});

	}

}]

More information about the ajaxSubmit api can be found in the documentation pages, api and options, that I strongly recommend.
In conclusion, I just want to invite you to note (again, if you read also my previous posts) the use of the $apply method, needed to reflect in AngularJS all the operations, which come from outside the framework.

The backend

For the sake of completeness this is the content of the uploader.php, that I used to upload the files.


move_uploaded_file($_FILES["uploader"]["tmp_name"], 
	"upload/" . $_FILES["uploader"]["name"]);

To be sincere there is a little more inside my original uploader.php, but this will be enough to a basic uploader.

Bruno

I'm Bruno Scopelliti, web developer from Bologna, Italy. I use this space to share my experiences, experiments and thoughts. I'm also on Google+, Twitter, and LinkedIn.

More Posts

Standard
  • Danial Farid

    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

  • Akm

    Can you provide a working example for this?

  • Peter Drinnan

    I was looking at the uploader.html partial and noticed is has the HTML head, body and content type tag. Is that necessary or just convention?

    • http://brunoscopelliti.com/ Bruno Scopelliti

      Hi Peter,
      The uploader.html template has not the head, and neither the body tag… It is exactly what I showed in this post…
      However if you open it in the browser, your browser completes the template adding the head, and the body tag.

      • Peter Drinnan

        Ah! Makes sense. Thanks for explaining.

  • Luke Mason

    This directive will fail in IE9 as IE doesn’t allow manipulation of the type=”file” input element from javascript for security reasons. Setting the filename or invoking a click event to show the browser dialog will result in an “Access is denied” error on the form submit. So be sure in ie9 to include a fallback.

    • Sam

      Who cares IE, the hell….

  • Anil Kumar Pandey

    Hi Bruno, Its very good directive for uploading file… but I have some problem. I am using java as a server side language so can you tell me how to get and move the uploaded file. Your blog has php as server side logic. Please give me some solution for this. Thanks

  • http://michaelcalkins.com/ Michael Calkins

    Easy file upload with simple progress. (Based on this post but with mods to make it simple to modify)
    https://github.com/clouddueling/angularjs-file-upload

  • Karel Frederix

    Really like this directive! In fact, I am using it in my latest project and it works a charm.
    One remark though: I like it better when the file input’s change handler is hooked up from inside the directive ‘link’ function.
    That’s how I have done it:

    link: function (scope, elem, attrs, ctrl) {
    elem.find(‘.fake-uploader’).find(‘button’).click(function () {
    elem.find(‘input[type="file"]‘).click();
    });
    var fileInputEl = elem.find(‘input[type=file]‘);
    fileInputEl.change(function () {
    scope.sendFile(fileInputEl[0]);
    });
    }

    • Lars Jeppesen

      Definitely an improvement. Makes the template less cluttered.

    • amanfs

      I updated to angular 1.2.10 from 1.1.5, and this directive stopped working. Further inspecting it I found the template scope isn’t the same as the directives. For the uploader sendFile function to work, you must do the way Karel has provided us. Thanks again!

  • Anil Kumar Pandey

    How to use same directive as a multi-uploader in a single form, because I have 3 uploader but I am facing problem while getting the image name, all are sharing same model. Can you have any solution for this.

  • Anil Kumar Pandey

    Hi bruno, any solution for using multiple file-uploader in a single page. I also asked same question in stackoverflow please follow this link http://stackoverflow.com/questions/19194209/angularjs-multiple-file-uploader-in-a-single-page

  • saradha

    Hi ,
    This file uploader works good.

    But i faced a problem that File uploader progress bar doesn’t visible in firefox brower. The uploaderProgress event not triggered in firefox

  • Lars Jeppesen

    Thanks Bruno. Really helpful

  • Narasimha

    Nice tutorial! Were you able to write e2e tests for file upload?

  • Verónica Artola

    Thanks Bruno! It was really helpful!

  • ziobudda

    Hi, is there any possibility to transform your directive from Element to Attribute ? If yes: how ? Thanks.