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?

8 Answers 8

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);
     });
};
Sign up to request clarification or add additional context in comments.

10 Comments

I've used this same technique successfully. It's ugly... but it's the best we have for now.
What goes into the ...?
@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 html5rocks.com/en/tutorials/file/dndfiles
Note that this won't work if you have debugInfo disabled (as recommended for production environments)
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.
|
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);
 };

2 Comments

Preview image in the view by adding: <img src="{{prev_img}}" />
The changed code is what is actually needed, was just beginning to wonder why my model doesnt update.
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); 

1 Comment

This is a more comprehensive approach.
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
}

Comments

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);
}

3 Comments

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 ...
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.
This is working for me, and is the simplest implementation that I have found so far.
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);
      }
    };
  }]);

1 Comment

html gotta use ngModel though. <input type="file" file-change="yourHandler($event, files)" ng-model="myFiles"/>
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>

Comments

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.