Smart Tech for a better Web

A simple drag/drop upload directive for AngularJS

von

As I found out, DropzoneJS doesn't fit all my needs when I work with AngularJS. I once made a directive that wrapped it. For some reason, it is not trivial to change the options for a running DropzoneJS object that prevents to use ng-models as an upload URL.

I decided to write a custom solution. It's not that difficult when you know your target browsers and what you need. It's difficult when you need to address older browsers. In my case, I have a closed group of users, and I can dictate a browser.

The following example should run on most recent desktop browsers: http://caniuse.com/#search=drag.

The idea is to create an AngularJS directive which will be used like that:

<upload to="/path/to/upload/script.php" ng-model="object.id"></upload>

It's an element that specifies the URL of the upload script and also links to a ng-model. The ng-model is, in fact, an existing row in the database. The ID will be used to create a proper directory.

Please note that hackers may manipulate values like the object.id and try to store something where it doesn't belong. Be careful with what you do and consider server side checks - which you should always have.

Here is the skeleton of my directive:

directives.directive('upload', ['$http', function($http) {
    return {
        restrict: 'E',
        replace: true,
        scope: {},
        require: '?ngModel',
        template: '<div class="asset-upload">Drag here to upload</div>',
        link: function(scope, element, attrs, ngModel) {

            // Code goes here

        }
    };
}]);

The above directive depends to $http (for uploading) and on a ngModel. It works just like an HTML element (restrict: 'E') and brings its markup. In this case, it is just a div with some text and a CSS class.

But let's look at the more interesting code. Please note, I am going to use some jQuery.

First we need to disable drag over and drag enter elements on our upload div (name element and provided by AngularJS). As I found out, images might be opened inside the browser instead of being uploaded otherwise.

element.on('dragover', function(e) {
    e.preventDefault();
    e.stopPropagation();
});
element.on('dragenter', function(e) {
    e.preventDefault();
    e.stopPropagation();
});

Next to the drop function. First we need to disable the default behavior. Then we can access the dataTransfer property of our event. I got the inspiration for this at Mozillas documentation.. Instead of using the getData method I found the files property that seems to hold all files I am dropping to the area.

element.on('drop', function(e) {
    e.preventDefault();
    e.stopPropagation();
    if (e.originalEvent.dataTransfer){
        if (e.originalEvent.dataTransfer.files.length > 0) {
            upload(e.originalEvent.dataTransfer.files);
        }
    }
    return false;
});

A few checks later I put these files into my upload function which does the job:

var upload = function(files) {
    var data = new FormData();
    angular.forEach(files, function(value){
        data.append("files[]", value);
    });

    data.append("objectId", ngModel.$viewValue);

    $http({
        method: 'POST',
        url: attrs.to,
        data: data,
        withCredentials: true,
        headers: {'Content-Type': undefined },
        transformRequest: angular.identity
    }).success(function() {
        console.log("Uploaded");
    }).error(function() {
        console.log("Error");
    });
};

I am working with a FormData object that is only supported by modern browsers.

The idea is simple: create a FormData instance and add files to it. Please note the array notation I used as a key. Finally, I added my ngModels value as object id.

Then there is just an $http call to perform. The URL comes from the attribute of my custom tag. What I am going to send is the FormData object.

I had some trouble finding out about these two settings:

headers: {'Content-Type': undefined },
transformRequest: angular.identity

I tried "multipart/form-data" as Content-Type, but it didn't work. In the dark corners of the Internet, I read about using undefined. I am sorry, I can't find the reference. :-(

Transformation Request: In AngularJS requests are transformed to JSON if an object is put into data. The angular.identity is a function which returns its first argument. So you can say the data portion is left untouched. It's a bit weird. If you have a better solution, feel free to add it to the comments.

On the server side, you can access with PHPs super global $_FILES and of course objectId as post field too. With Java, you should be able to read the files from the Servlet Context.

Tags: #AngularJS #Upload #Drag and Drop #HTML5 #JavaScript

Newsletter

ABMELDEN

BLOG-POST TEILEN