290

I am new to angular. I am trying to read the uploaded file path from HTML 'file' field whenever a 'change' happens on this field. If i use 'onChange' it works but when i use it angular way using 'ng-change' it doesn't work.

<script>
   var DemoModule = angular.module("Demo",[]);
   DemoModule .controller("form-cntlr",function($scope){
   $scope.selectFile = function()
   {
        $("#file").click();
   }
   $scope.fileNameChaged = function()
   {
        alert("select file");
   }
});
</script>

<div ng-controller="form-cntlr">
    <form>
         <button ng-click="selectFile()">Upload Your File</button>
         <input type="file" style="display:none" 
                          id="file" name='file' ng-Change="fileNameChaged()"/>
    </form>  
</div>

fileNameChaged() is never calling. Firebug also doesn't show any error.

MayurB
  • 3,609
  • 2
  • 21
  • 36
Manish Kumar
  • 10,214
  • 25
  • 77
  • 147

15 Answers15

495

I made a small directive to listen for file input changes.

View JSFiddle

view.html:

<input type="file" custom-on-change="uploadFile">

controller.js:

app.controller('myCtrl', function($scope){
    $scope.uploadFile = function(event){
        var files = event.target.files;
    };
});     

directive.js:

app.directive('customOnChange', function() {
  return {
    restrict: 'A',
    link: function (scope, element, attrs) {
      var onChangeHandler = scope.$eval(attrs.customOnChange);
      element.on('change', onChangeHandler);
      element.on('$destroy', function() {
        element.off();
      });

    }
  };
});
Alexander Derck
  • 13,818
  • 5
  • 54
  • 76
sqren
  • 22,833
  • 7
  • 52
  • 36
  • 4
    This works well if a different file is chosen each time. Is it possible to listen for when the same file is selected as well? – JDawg Sep 11 '15 at 21:01
  • 13
    This has one very unfortunate "bug" - the controller is not bound as "this" in the customOnChange, but the file input! It threw me off for a long time and I couldn't find the actual bug. I am not yet sure how to actually call it with correctly set "this". – Karel Bílek Oct 19 '15 at 16:59
  • 4
    the fiddle does not work for me, either on Chrome or Firefox, as of today. – seguso Dec 06 '15 at 09:41
  • 2
    This answer is flawed as stated by @KarelBílek. I couldn't get around it and decided to use angular-file-upload. – Gunar Gessner Dec 22 '15 at 19:26
  • @sqren, second time without selecting any file, if i click on `cancel` button, already selected file name is getting reset to 'No file chosen', can you suggest some way to handle this case. – Uppicharla Jan 15 '16 at 06:54
  • 1
    @Karel Bílek you can try binding the directives scope with ```element.bind('change', onChangeHandler.bind(scope));```. Be careful here because this will change depending on how you structured your app. I was able to access the 'this' that I needed in my controller which was on the prototype object 'vm'. Hope that helps. – Patrick Michalina May 11 '16 at 02:32
  • 2
    IE and maybe even Chrome doesn't like when you re-upload a file with the same name. Here is how to fix that: http://stackoverflow.com/questions/12030686/html-input-file-selection-event-not-firing-upon-selecting-the-same-file#answer-12102992 – mellis481 Jun 01 '16 at 19:10
  • 1
    This stops working if you select the same file twice from the system prompt. – janhocevar Jul 25 '16 at 06:23
  • 2
    To get this to work, i had to implement the changes suggested by @PatrickMichalina and, because the function executes in the directive's isolate scope, also add a `$scope.$apply()` call at the end of the function. It works with multiple selections of the same file with the changes from the fiddle linked by @sqren in his Sept 12 '15 comment. – The Head Rush Aug 30 '16 at 20:26
  • Can you also access the files content with this method. I have a file which I only need to upload to the frontend. – Andi Giga Sep 15 '16 at 20:17
  • 1
    This is a great solution. But it is missing one bit. When the input is updated, the form should be marked as dirty. Add `require: '^form',` to the directive definition, pass the form controller to the link function, then call `form.$setDirty()` in the change handler function. – nmgeek Oct 16 '16 at 01:47
  • What does `restrict: 'A'` do? – lmaooooo Oct 18 '16 at 13:51
  • @makz `restrict: 'A'` restricts the directive to be used as attribute name. https://docs.angularjs.org/guide/directive Other values are `E - element, C - class, M - comment` . You can use combination `ACE` which means it can be anywhere in the combination. – bhantol Jan 05 '17 at 15:22
  • I have some issues with the scope, posted `plnkr` link to the `chat`: http://chat.stackoverflow.com/transcript/message/35411481#35411481 – Mars Robertson Jan 31 '17 at 22:42
  • 2
    This is absolutely how to do it, worked like a charm. Besides, why would you use a debug feature in production? – Justin Kruse Feb 12 '17 at 16:47
  • If a file is opened and when we try uploading in the file in Microsoft Edge browser. Nothing is happening. Can you help? – Naveen Katakam Feb 23 '17 at 12:24
  • 1
    @shaikh this is the better way. When I left that comment 3 years ago, this answer didn't have the most votes. Now it does, making it look like I was saying that this answer is not good. But, this answer is good. This is how I would do it. – frosty Oct 26 '17 at 15:29
  • @KarelBílek I think I had the same issue. Just make sure to set a unique `id` on the input and it should work. – smukov Jan 11 '18 at 15:49
  • JS fiddle doesn't work. Shame - it seemed like a short and nice solution... – Marek M. Jan 18 '18 at 17:06
  • I thought it didn't bind this correctly, as @KarelBílek mentioned, but then realized it is binding fine, except the view didn't refresh, so this line helped `element.on('change', (...args) => scope.$apply(() => onChangeHandler(...args)));` I don't know if it is the most ideomatic way doing this – Igor Sukharev Sep 08 '18 at 17:14
  • Works! The input file should be cleared to upload same file multiple times. Like @sqren shows in the fiddle. Thanks. – Shnigi Oct 31 '18 at 12:35
250

No binding support for File Upload control

https://github.com/angular/angular.js/issues/1375

<div ng-controller="form-cntlr">
        <form>
             <button ng-click="selectFile()">Upload Your File</button>
             <input type="file" style="display:none" 
                id="file" name='file' onchange="angular.element(this).scope().fileNameChanged(this)" />
        </form>  
    </div>

instead of

 <input type="file" style="display:none" 
    id="file" name='file' ng-Change="fileNameChanged()" />

can you try

<input type="file" style="display:none" 
    id="file" name='file' onchange="angular.element(this).scope().fileNameChanged()" />

Note: this requires the angular application to always be in debug mode. This will not work in production code if debug mode is disabled.

and in your function changes instead of

$scope.fileNameChanged = function() {
   alert("select file");
}

can you try

$scope.fileNameChanged = function() {
  console.log("select file");
}

Below is one working example of file upload with drag drop file upload may be helpful http://jsfiddle.net/danielzen/utp7j/

Angular File Upload Information

URL for AngularJS File Upload in ASP.Net

https://github.com/geersch/AngularJSFileUpload

AngularJs native multi-file upload with progress with NodeJS

http://jasonturim.wordpress.com/2013/09/12/angularjs-native-multi-file-upload-with-progress/

ngUpload - An AngularJS Service for uploading files using iframe

http://ngmodules.org/modules/ngUpload

Christophe Geers
  • 8,564
  • 3
  • 37
  • 53
JQuery Guru
  • 6,943
  • 1
  • 33
  • 40
  • It worked!!! one more question how can i get the file path from this file input type? – Manish Kumar Jul 29 '13 at 12:35
  • 2
    onchange="angular.element(this).scope().fileNameChaged(this)" $scope.fileNameChaged = function(element) { console.log('files:', element.files); } Can you change in two place and check ? depending on browser you will get the file full path i have update the fiddle below is the url upload file and check console http://jsfiddle.net/utp7j/260/ – JQuery Guru Jul 29 '13 at 12:43
  • I am using in mozilla and its giving [object FileList]. How will i get the file path – Manish Kumar Jul 29 '13 at 12:52
  • 2
    for (var i = 0; i < element.files.length; i++) { console.log(element.files[i]) } When you loop over the files object in mozilla attribut is "mozFullPath" if you find my answer useful you can upvote also – JQuery Guru Jul 29 '13 at 13:04
  • It worked but dont you think its a bit complicated compared to jQuery $("#file").text(). – Manish Kumar Jul 30 '13 at 05:35
  • You are not suppose to do any DOM manipulation in angular controllers, thus $("#file").text() should not be used. The point is angular controllers can be tested without an actual DOM attached to it. – user658991 Nov 11 '13 at 04:37
  • 14
    This method circumvents the default AngularJS processing, so $scope.$digest() isn't called (bindings aren't updated). Call 'angular.element(this).scope().$digest()' afterwards or call a scope method that uses $apply. – Ramon de Klein May 26 '14 at 09:01
  • This works, but there is a bug. Say you open the browse dialog (the input box), select multiple files, and retrieve those files in a callback that is listening for an 'onchange'. You will receive that list of files. Now, I have a dialog where the user can delete that list of files and select a new list. If the user selects the same list as before, the 'onchange' event listener is not triggered because the value of the input box did not change! Can I detect an 'onclick' event from the 'Open' button on the browse dialog only? – WebWanderer Jul 23 '14 at 14:17
  • I just solved that issue. In the callback of your 'onchange' event listener, make sure that you set the value of your input to "" in order to re-trigger the 'onchange' event next time. – WebWanderer Jul 23 '14 at 14:44
  • 120
    This is a horrible answer. Calling angular.element(blah).scope() is a debug feature that you should only use for debugging. Angular doesn't use the .scope feature. It is only there to help developers debug. When you build your app and deploy to production, you are supposed to turn debug info off. This speeds up your all A LOT!!! So, to write code that depends on element.scope() calls is a bad idea. Don't do this. The answer about creating a custom directive is the right way to do this. @JQueryGuru you should update your answer. Because it has the most votes, people will follow this bad advice. – frosty Dec 17 '14 at 23:50
  • 1
    To learn more about turning off debug data to speed up your app, read the Angular production guide. It talks about the speed you gain by turning it off and how to do it: https://docs.angularjs.org/guide/production – frosty Dec 17 '14 at 23:51
  • 7
    This solution will break the app when debug info is disabled. Disabling that for production is an official recommendation https://docs.angularjs.org/guide/production. Please see also http://blog.thoughtram.io/angularjs/2014/12/22/exploring-angular-1.3-disabling-debug-info.html – wiherek Feb 13 '15 at 18:31
  • 4
    BAD SOLUTION ... ... read other comments about 'turning off debugging' ... ... ... and see `sqren`'s solution ... ... ... @0MV1 – dsdsdsdsd Mar 14 '16 at 16:39
  • Perfect, i try so many things to preform an individual or multiple row selection with the same event, but failed, with that line "onchange="angular.element(this).scope().fileNameChanged()" of creative call I can do easily WHAT I NEED.. – QasimRamzan Sep 26 '16 at 14:12
  • This is a great solution if you don't mind your site being hacked. – Michael Warner May 19 '17 at 14:48
  • 1
    @manish as many pointed out, this answer is very bad... and it can mislead angular beginners. I nearly got fooled too before investigating a little bit. Can you reconsider your accepted answer? Thanks! – Frederik.L Jul 06 '17 at 21:13
  • These comments address all the flaws with this solution, not to mention it involves placing unnecessary logic in the template. Check out my solution. It is the correct way to handle this situation. Cheers. – Matt S Sep 08 '17 at 00:45
43

This is a refinement of some of the other ones around, the data will end up in an ng-model, which is normally what you want.

Markup (just make an attribute data-file so the directive can find it)

<input
    data-file
    id="id_image" name="image"
    ng-model="my_image_model" type="file">

JS

app.directive('file', function() {
    return {
        require:"ngModel",
        restrict: 'A',
        link: function($scope, el, attrs, ngModel){
            el.bind('change', function(event){
                var files = event.target.files;
                var file = files[0];

                ngModel.$setViewValue(file);
                $scope.$apply();
            });
        }
    };
});
Stuart Axon
  • 1,844
  • 1
  • 26
  • 44
  • 1
    Is `$scope.$apply();` required? My `ng-change` event still seems to fire without it. – row1 Jan 19 '15 at 23:02
  • 1
    @row1 you only need to manually fire an apply if you have other angular things that need to know about it *during* that operation. – Scott Sword Jul 07 '15 at 22:24
27

The clean way is to write your own directive to bind to "change" event. Just to let you know IE9 does not support FormData so you cannot really get the file object from the change event.

You can use ng-file-upload library which already supports IE with FileAPI polyfill and simplify the posting the file to the server. It uses a directive to achieve this.

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

<div ng-controller="MyCtrl">
  <input type="file" ngf-select="onFileSelect($files)" multiple>
</div>

JS:

//inject angular file upload directive.
angular.module('myApp', ['ngFileUpload']);

var MyCtrl = [ '$scope', 'Upload', function($scope, Upload) {
  $scope.onFileSelect = function($files) {
    //$files: an array of files selected, each file has name, size, and type.
    for (var i = 0; i < $files.length; i++) {
      var $file = $files[i];
      Upload.upload({
        url: 'my/upload/url',
        data: {file: $file}
      }).then(function(data, status, headers, config) {
        // file is uploaded successfully
        console.log(data);
      }); 
    }
  }
}];
danial
  • 4,058
  • 2
  • 32
  • 39
26

I've expanded on @Stuart Axon's idea to add two-way binding for the file input (i.e. allow resetting the input by resetting the model value back to null):

app.directive('bindFile', [function () {
    return {
        require: "ngModel",
        restrict: 'A',
        link: function ($scope, el, attrs, ngModel) {
            el.bind('change', function (event) {
                ngModel.$setViewValue(event.target.files[0]);
                $scope.$apply();
            });

            $scope.$watch(function () {
                return ngModel.$viewValue;
            }, function (value) {
                if (!value) {
                    el.val("");
                }
            });
        }
    };
}]);

Demo

JLRishe
  • 99,490
  • 19
  • 131
  • 169
  • Thank you @JLRishe. One note , actually `data-ng-click="vm.theFile=''"` works well , no need to write it to a controller method – Sergey Apr 27 '16 at 23:33
21

Similar to some of the other good answers here, I wrote a directive to solve this problem, but this implementation more closely 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
  • struggled for a couple days with this until i tried your answer. Thanks! – MikeT Dec 27 '16 at 20:52
  • How could I pass an additional parameter to the `fileChange` expression? I'm using this directive inside an `ng-repeat` and the `$scope` variable passed to `attrHandler` is the same for each record. What's the best solution? –  Apr 19 '17 at 12:56
18

This directive pass the selected files as well:

/**
 *File Input - custom call when the file has changed
 */
.directive('onFileChange', function() {
  return {
    restrict: 'A',
    link: function (scope, element, attrs) {
      var onChangeHandler = scope.$eval(attrs.onFileChange);

      element.bind('change', function() {
        scope.$apply(function() {
          var files = element[0].files;
          if (files) {
            onChangeHandler(files);
          }
        });
      });

    }
  };
});

The HTML, how to use it:

<input type="file" ng-model="file" on-file-change="onFilesSelected">

In my controller:

$scope.onFilesSelected = function(files) {
     console.log("files - " + files);
};
Balazs Nemeth
  • 2,333
  • 19
  • 29
8

I recommend to create a directive

<input type="file" custom-on-change handler="functionToBeCalled(params)">

app.directive('customOnChange', [function() {
        'use strict';

        return {
            restrict: "A",

            scope: {
                handler: '&'
            },
            link: function(scope, element){

                element.change(function(event){
                    scope.$apply(function(){
                        var params = {event: event, el: element};
                        scope.handler({params: params});
                    });
                });
            }

        };
    }]);

this directive can be used many times, it uses its own scope and doesn't depend on parent scope. You can also give some params to handler function. Handler function will be called with scope object, that was active when you changed the input. $apply updates your model each time the change event is called

Kampai
  • 22,848
  • 21
  • 95
  • 95
Julia Usanova
  • 437
  • 5
  • 6
4

The simplest Angular jqLite version.

JS:

.directive('cOnChange', function() {
    'use strict';

    return {
        restrict: "A",
        scope : {
            cOnChange: '&'
        },
        link: function (scope, element) {
            element.on('change', function () {
                scope.cOnChange();
        });
        }
    };
});

HTML:

<input type="file" data-c-on-change="your.functionName()">
Jonáš Krutil
  • 245
  • 1
  • 9
2

Working Demo of "files-input" Directive that Works with ng-change1

To make an <input type=file> element work the ng-change directive, it needs a custom directive that works with the ng-model directive.

<input type="file" files-input ng-model="fileList" 
       ng-change="onInputChange()" multiple />

The DEMO

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

.controller("ctrl", function($scope) {
     $scope.onInputChange = function() {
         console.log("input change");
     };
})
<script src="//unpkg.com/angular/angular.js"></script>
  <body ng-app="app" ng-controller="ctrl">
    <h1>AngularJS Input `type=file` Demo</h1>
    
    <input type="file" files-input ng-model="fileList" 
           ng-change="onInputChange()" multiple />
    
    <h2>Files</h2>
    <div ng-repeat="file in fileList">
      {{file.name}}
    </div>
  </body>
georgeawg
  • 48,608
  • 13
  • 72
  • 95
  • Great but directive is only called once ! If I change file selected then directive is not called a second time ! Do you have any idea about this behavior ? – hugsbrugs Apr 05 '20 at 15:59
  • The DEMO doesn't have that problem. Create a new question with a [reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) for readers to examine. – georgeawg Apr 05 '20 at 17:35
  • yes it have that problem the console log "input change" only displays once even if you change selected files many times ! – hugsbrugs Apr 05 '20 at 19:10
  • after testing, it works on chromium but it doesn't on latest firefox version ... damn browser compatibilty !!! – hugsbrugs Apr 06 '20 at 14:45
1

Too complete solution base on:

`onchange="angular.element(this).scope().UpLoadFile(this.files)"`

A simple way to hide the input field and replace it with a image, here after a solution, that also require a hack on angular but that do the job [TriggerEvent does not work as expected]

The solution:

  • place the input-field in display:none [the input field exist in the DOM but is not visible]
  • place your image right after On the image use nb-click() to activate a method

When the image is clicked simulate a DOM action 'click' on the input field. Et voilà!

 var tmpl = '<input type="file" id="{{name}}-filein"' + 
             'onchange="angular.element(this).scope().UpLoadFile(this.files)"' +
             ' multiple accept="{{mime}}/*" style="display:none" placeholder="{{placeholder}}">'+
             ' <img id="{{name}}-img" src="{{icon}}" ng-click="clicked()">' +
             '';
   // Image was clicked let's simulate an input (file) click
   scope.inputElem = elem.find('input'); // find input in directive
   scope.clicked = function () {
         console.log ('Image clicked');
         scope.inputElem[0].click(); // Warning Angular TriggerEvent does not work!!!
    };
Fulup
  • 545
  • 3
  • 14
1

Another interesting way to listen to file input changes is with a watch over the ng-model attribute of the input file. Of course, FileModel is a custom directive.

Like this:

HTML -> <input type="file" file-model="change.fnEvidence">

JS Code ->

$scope.$watch('change.fnEvidence', function() {
                    alert("has changed");
                });

Hope it can help someone.

halbano
  • 1,135
  • 14
  • 34
0

I have done it like this;

<!-- HTML -->
<button id="uploadFileButton" class="btn btn-info" ng-click="vm.upload()">    
<span  class="fa fa-paperclip"></span></button>
<input type="file" id="txtUploadFile" name="fileInput" style="display: none;" />
// self is the instance of $scope or this
self.upload = function () {
   var ctrl = angular.element("#txtUploadFile");
   ctrl.on('change', fileNameChanged);
   ctrl.click();
}

function fileNameChanged(e) {
    console.log(self.currentItem);
    alert("select file");
}
Ali Sakhi
  • 195
  • 1
  • 1
  • 9
0

Angular elements (such as the root element of a directive) are jQuery [Lite] objects. This means we can register the event listener like so:

link($scope, $el) {
    const fileInputSelector = '.my-file-input'

    function setFile() {
        // access file via $el.find(fileInputSelector).get(0).files[0]
    }

    $el.on('change', fileInputSelector, setFile)
}

This is jQuery event delegation. Here, the listener is attached to the root element of the directive. When the event is triggered, it will bubble up to the registered element and jQuery will determine if the event originated on an inner element matching the defined selector. If it does, the handler will fire.

Benefits of this method are:

  • the handler is bound to the $element which will be automatically cleaned up when the directive scope is destroyed.
  • no code in the template
  • will work even if the target delegate (input) has not yet been rendered when you register the event handler (such as when using ng-if or ng-switch)

http://api.jquery.com/on/

Matt S
  • 1,892
  • 16
  • 15
-1

You can simply add the below code in onchange and it will detect change. you can write a function on X click or something to remove file data..

document.getElementById(id).value = "";
Jijo Thomas
  • 875
  • 1
  • 10
  • 14