27

I would like to pick up a file with AngularJS:

HTML:

<div ng-controller="TopMenuCtrl">
    <button class="btn" ng-click="isCollapsed = !isCollapsed">Toggle collapse</button>
    <input type="file" ng-model="filepick" ng-change="pickimg()" multiple />
    <output id="list"></output> 
</div>

javascript:

angular.module('plunker', ['ui.bootstrap']);
function TopMenuCtrl($scope) {
    $scope.pickimg = function() {
        alert('a');
    };
}

How can I bind the input file onchange action on the AngularJS pickimg function? And how can i manipulate the files uploaded after?

Sr.Richie
  • 5,680
  • 5
  • 38
  • 62
mcbjam
  • 7,344
  • 10
  • 32
  • 40

8 Answers8

49

Angular doesn't yet support ng-change for input[type=file] so you have to roll onchange implementation yourself.

First, in the HTML, define Javascript for onchange as follows:

<input ng-model="photo"
       onchange="angular.element(this).scope().file_changed(this)"
       type="file" accept="image/*" />

And then in your Angular controller code, define the function:

$scope.file_changed = function(element) {

     $scope.$apply(function(scope) {
         var photofile = element.files[0];
         var reader = new FileReader();
         reader.onload = function(e) {
            // handle onload
         };
         reader.readAsDataURL(photofile);
     });
};
lippertsjan
  • 309
  • 1
  • 18
Teemu Kurppa
  • 4,779
  • 2
  • 32
  • 38
  • 2
    I've used this same technique successfully. It's ugly... but it's the best we have for now. – Chris Esplin Jul 03 '13 at 17:37
  • What goes into the ...? – user1876508 Jul 18 '13 at 20:59
  • @user1876508 "After you've obtained a File reference, instantiate a FileReader object to read its contents into memory. When the load finishes, the reader's onload event is fired and its result attribute can be used to access the file data." From http://www.html5rocks.com/en/tutorials/file/dndfiles/ – Mario Levrero Feb 27 '14 at 15:26
  • @teemu-kurppa works like a charm.. I only have issue, that how can I add iterating ngRepeat element into `.file_changed(this)` e.g. `.file_changed(this,obj)` where obj is `ngRepeat="obj in objects"` When I tried, it says obj is not defined. – Aman Gupta Oct 22 '14 at 01:44
  • @agpt you need to read it from the scope. onchange is a browser event and happens outside of angular scopes. The same way that file_changed is not directly accessible. – Teemu Kurppa Oct 23 '14 at 06:27
  • The `$scope.$apply(function(scope) {` part should be inside the `reader.onload`. Otherwise the model does not update. – Vincent P Jun 17 '15 at 07:36
  • 3
    Note that this won't work if you have [debugInfo](https://docs.angularjs.org/guide/production#disabling-debug-data) disabled (as recommended for production environments) – Bruno Peres Sep 09 '15 at 18:57
  • 1
    note that you can get $element in your directive controller (or grab the element ref in your link function), so you can do something like: `$element[0].children[0].onchange = () => {}` in your controller setup. This removes the need for having to run in debug mode, since you can grab the element while still having access to your controller scope. – pfooti May 04 '16 at 21:30
  • Any chance of some code for that? A complete example, maybe as a Plunk? – Mawg says reinstate Monica Jan 22 '17 at 11:43
  • This answer use a debugging option of the AngularJS framework. It shouldn't be used in production because it detracts from performance. There are better answers. – georgeawg Feb 25 '19 at 18:48
17

I used above method trying to load a preview image when new file is selected, however it didnt work when I tried it like that:

$scope.file_changed = function(element, $scope) {

     $scope.$apply(function(scope) {
         var photofile = element.files[0];
         var reader = new FileReader();
         reader.onload = function(e) {
            $scope.prev_img = e.target.result;
         };
         reader.readAsDataURL(photofile);
     });
});

I digged more into it and found that the $scope.$apply should be inside the reader.onLoad otherwise changing a $scope variables wont work, so I did the following and it worked:

$scope.file_changed = function(element) {

        var photofile = element.files[0];
        var reader = new FileReader();
        reader.onload = function(e) {
            $scope.$apply(function() {
                $scope.prev_img = e.target.result;
            });
        };
        reader.readAsDataURL(photofile);
 };
Gopesh
  • 3,882
  • 11
  • 37
  • 52
Sharon Abu
  • 91
  • 2
  • 6
6

Teemu solution will not work for IE9.

I have put together a simple angular directive with Flash polyfill for browsers not supporting HTML5 FormData, you can also listen to upload progress event.

https://github.com/danialfarid/ng-file-upload Demo: http://angular-file-upload.appspot.com/

<script src="angular.min.js"></script>
<script src="ng-file-upload.js"></script>

<div ng-controller="MyCtrl">
  <input type="text" ng-model="additionalData">
  <div ngf-select ng-model="files" >
</div>

controller:

Upload.upload({
    url: 'my/upload/url',
    data: additionalData,
    file: files
  }).then(success, error, progress); 
danial
  • 4,058
  • 2
  • 32
  • 39
4

Following is my approach with a directive.

Directive

angular
  .module('yourModule')
  .directive('fileChange', function() {
    return {
     restrict: 'A',
     scope: {
       handler: '&'
     },
     link: function (scope, element) {
      element.on('change', function (event) {
        scope.$apply(function(){
          scope.handler({files: event.target.files});
        });
      });
     }
    };
});

HTML

<input type="file" file-change handler="fileSelect(files)">

Controller

fileSelect = function (files) {
      var file = files[0];
      //you will get the file object here
}
Madura Pradeep
  • 2,378
  • 1
  • 30
  • 34
3

Using Madura's answer from above, here's the complete flow for reading a local JSON file:

Create directive:

angular
  .module('app.services')
  .directive('fileChange', function() {
    return {
     restrict: 'A',
     scope: {
       handler: '&'
     },
     link: function (scope, element) {
      element.on('change', function (event) {
        scope.$apply(function(){
          scope.handler({files: event.target.files});
        });
      });
     }
    };
});

HTML:

<input type="file" file-change handler="fileSelect(files)">

Javascript:

$scope.fileSelect = function(files) {
  var file = files[0];
  var reader = new FileReader();
  reader.onload = function(e) {
    console.log("on load", e.target.result);
  }
  reader.readAsText(file);
}
Community
  • 1
  • 1
Snowman
  • 31,411
  • 46
  • 180
  • 303
  • When I try it, it won't breakpoint in `this.fileSelect()`. When I change it to `$scope.fileSelect()`, it does breakpoint, but reports only the file's name, not a complete path. Can you help? There are a lot of fiendishly complex AngulrJs file selector examples out there, and this is the only one that seems simple enough to grok. So close, and yet ... – Mawg says reinstate Monica Jan 22 '17 at 11:39
  • 1
    You're right, it should be $scope and not `this`. As far as file path, I think the behavior you're seeing is the correct behavior. You should still be able to read the file based on the results you get. – Snowman Jan 22 '17 at 17:09
  • This is working for me, and is the simplest implementation that I have found so far. – Mawg says reinstate Monica Jan 22 '17 at 22:37
2

Here's a lightweight directive I wrote to solve this problem, which mirrors the angular way of attaching events.

You can use the directive like this:

HTML

<input type="file" file-change="yourHandler($event, files)" />

As you can see, you can inject the files selected into your event handler, as you would inject an $event object into any ng event handler.

Javascript

angular
  .module('yourModule')
  .directive('fileChange', ['$parse', function($parse) {

    return {
      require: 'ngModel',
      restrict: 'A',
      link: function ($scope, element, attrs, ngModel) {

        // Get the function provided in the file-change attribute.
        // Note the attribute has become an angular expression,
        // which is what we are parsing. The provided handler is 
        // wrapped up in an outer function (attrHandler) - we'll 
        // call the provided event handler inside the handler()
        // function below.
        var attrHandler = $parse(attrs['fileChange']);

        // This is a wrapper handler which will be attached to the
        // HTML change event.
        var handler = function (e) {

          $scope.$apply(function () {

            // Execute the provided handler in the directive's scope.
            // The files variable will be available for consumption
            // by the event handler.
            attrHandler($scope, { $event: e, files: e.target.files });
          });
        };

        // Attach the handler to the HTML change event 
        element[0].addEventListener('change', handler, false);
      }
    };
  }]);
Simon Robb
  • 1,668
  • 1
  • 14
  • 25
0

I have made a directive. Here is the fiddle.
The application works for picking csvs and showing them as html tables.
With on-file-change directive, you would be able to define the file reading and parsing (with services, may be) logic in the controllers itself which will provide more flexibility. Just for the note, the ac.onFileChange function passed to on-file-change attribute will become the handler for input change event inside directive.

(function (angular, document) {

   angular
      .module("app.directives", [])
      .directive("onFileChange", ["$parse", function ($parse) {
         return {
            restrict: "A",
            link: function (scope, ele, attrs) {
               // onFileChange is a reference to the same function which you would define 
               // in the controller. So that you can keep your logic in the controller.
               var onFileChange = $parse(attrs.onFileChange.split(/\(/)[0])(scope)
               ele.on("change", onFileChange)
               ele.removeAttr("on-file-change")
            }
         }
      }])

   angular
      .module("app.services", [])
      .service("Parse", ["$q", function ($q) {
         var Parse = this
         Parse.csvAsGrid = function (file) {
            return $q(function (resolve, reject) {
               try {
                  Papa.parse(file, {
                     complete: function (results) {
                        resolve(results.data)
                     }
                  })
               } catch (e) {
                  reject(e)
               }
            })
         }
      }])

   angular
      .module("app", ["app.directives", "app.services"])
      .controller("appCtrl", ["$scope", "Parse", function ($scope, Parse) {
         var ac = this
         ac.fileName = ""
         ac.onFileChange = function (event) {
            if (!event.target.files.length) {
               return
            }
            Parse.csvAsGrid(event.target.files[0]).then(outputAsTable)
         }

         ac.clearInput = function (event) {
            var input = angular.element(event.target)
            input.val("")
            document.getElementById("output").innerHTML = ""
         }

         function outputAsTable(grid) {
            var table = ['<table border="1">']
            grid.map(function (row) {
               table.push('<tr>')
               row.map(function (cell) {
                  table.push('<td>' + cell.replace(/["']/g, "") + '</td>')
               })
               table.push('</tr>')
            })
            table.push('</table>')
            document.getElementById("output").innerHTML = table.join("\n")
         }
      }])

})(angular, document)
table {
  border-collapse: collapse;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.0.0/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.8/angular.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/PapaParse/4.1.2/papaparse.min.js"></script>

<div ng-app="app" ng-controller="appCtrl as ac">
  <label>Select a comma delimited CSV file:-</label>  
  <input id="filePicker" type="file" on-file-change="ac.onFileChange(event)" ng-click="ac.clearInput($event)"/>{{ac.fileName}}  
</div>
<div id="output"></div>
Vikas Gautam
  • 1,793
  • 22
  • 21
0

Directive that uses the ng-model-controller:

app.directive("selectNgFiles", function() {
  return {
    require: "ngModel",
    link: function postLink(scope,elem,attrs,ngModel) {
      elem.on("change", function(e) {
        var files = elem[0].files;
        ngModel.$setViewValue(files);
      })
    }
  }
});

Usage:

<input type="file" select-ng-files ng-model="fileArray"
       ng-change="pickimg()" multiple>

For more information, see Working Demo of Directive that Works with ng-model.

georgeawg
  • 48,608
  • 13
  • 72
  • 95