287

I tried to use ng-model on input tag with type file:

<input type="file" ng-model="vm.uploadme" />

But after selecting a file, in controller, $scope.vm.uploadme is still undefined.

How do I get the selected file in my controller?

georgeawg
  • 46,994
  • 13
  • 63
  • 85
Endy Tjahjono
  • 22,572
  • 20
  • 76
  • 111
  • 3
    See http://stackoverflow.com/a/17923521/135114, especially the cited example online at http://jsfiddle.net/danielzen/utp7j/ – Daryn Sep 11 '13 at 22:28
  • I believe you always need to specify the name property on the html element when using ngModel. – Sam Jan 03 '14 at 01:05

13 Answers13

331

I created a workaround with directive:

.directive("fileread", [function () {
    return {
        scope: {
            fileread: "="
        },
        link: function (scope, element, attributes) {
            element.bind("change", function (changeEvent) {
                var reader = new FileReader();
                reader.onload = function (loadEvent) {
                    scope.$apply(function () {
                        scope.fileread = loadEvent.target.result;
                    });
                }
                reader.readAsDataURL(changeEvent.target.files[0]);
            });
        }
    }
}]);

And the input tag becomes:

<input type="file" fileread="vm.uploadme" />

Or if just the file definition is needed:

.directive("fileread", [function () {
    return {
        scope: {
            fileread: "="
        },
        link: function (scope, element, attributes) {
            element.bind("change", function (changeEvent) {
                scope.$apply(function () {
                    scope.fileread = changeEvent.target.files[0];
                    // or all selected files:
                    // scope.fileread = changeEvent.target.files;
                });
            });
        }
    }
}]);
Endy Tjahjono
  • 22,572
  • 20
  • 76
  • 111
  • How do I access the image data from the controller? – Per Quested Aronsson Sep 18 '13 at 19:15
  • @PerQuestedAronsson through $scope.uploadme (like in the question). – Endy Tjahjono Sep 19 '13 at 03:58
  • 4
    I use uploadme as src in an img tag, so I can see it is getting set by the directive. However, if I try to grab it from the controller using $scope.uploadme, it is "undefined". I can set uploadme from the controller, though. For example, $scope.uploadme="*" makes the image disappear. – Per Quested Aronsson Sep 19 '13 at 18:15
  • @PerQuestedAronsson maybe create a new question with code sample included? – Endy Tjahjono Sep 20 '13 at 04:22
  • 5
    The problem is that the directive creates a childScope, and sets uploadme in that scope. The original (parent) scope also has an uploadme, which is unaffected by the childScope. I can update uploadme in the HTML from either scope. Is there a way to avoid creating a childScope at all? – Per Quested Aronsson Sep 20 '13 at 06:54
  • @EndyTjahjono it looks like the file definition is not enough to upload it. Am I missing something? – Alex C Feb 17 '15 at 15:05
  • 4
    @AlexC well the question was about ng-model not working, not about uploading files :) At that time I didn't need to upload the file. But recently I learned how to upload file from this [egghead.io tutorial](https://egghead.io/lessons/angularjs-file-uploads). – Endy Tjahjono Feb 18 '15 at 00:52
  • This doesn't work with multiple input type file elements. Is there any way to make it work? – TechnoCrat Apr 17 '15 at 12:25
  • @TechnoCrat you can do different thing when handling the change event, for example getting all of changeEvent.target.files, not just the first one like I did. See the two commented lines on my answer. – Endy Tjahjono Apr 17 '15 at 14:54
  • Tip: If you find that your controller variable is null, you may need to bind it to a parent scope like ``. I found this with *ionic* because many of it's directives create a child scope. I had to use `$parent.$parent.$parent.newImage` to get up to my root controller. – Simon East Oct 16 '15 at 07:56
  • 11
    don't forget to $scope.$on('$destory', function(){ element.unbind("change"); } – Nawlbergs Feb 23 '16 at 20:09
  • i think it is better to assign the file name in ng-model over the whole content of the file. – LoveToCode Mar 07 '16 at 08:31
  • @EndyTjahjono I have had a nightmare writing a unit test for this, any suggestions, I can't seem to get the change function to fire in the unit test. – James Mar 10 '16 at 18:32
  • Can this directive support "PDF" uploading? Is pdf file supporting dataurl reading? – Jeff Tian Mar 18 '16 at 09:48
  • 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 `$apply` function. – nmgeek Oct 16 '16 at 01:46
  • 3
    I have a question.... Isn't this way too complicated compared to plain javascript and html? Seriously, you really need to understand AngularJS to reach this solution... and it seems I could do se same with a javascript event. Why doing it the Angular way and not the plain JS way? – AFP_555 Mar 21 '17 at 00:40
  • `$scope.$on('$destroy', function(){ element.unbind("change"); });` corrected code of @Nawlbergs – Kugan Kumar Nov 30 '17 at 08:46
  • to validate if the input it's been cleared use: (changeEvent.target.files[0] instanceof Blob) which will give true or false, in case of false, scope.fileread = '' – alexOtano Jul 25 '18 at 17:23
  • @Endy Tjahjono Above solution is not working for macos firefox, chrome and safari browsers. – rajesh Apr 17 '19 at 07:57
  • The `reader.readAsDataURL` method is obsolete. Modern code uses [URL.create​ObjectURL()](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL). – georgeawg May 12 '19 at 23:21
53

I use this directive:

angular.module('appFilereader', []).directive('appFilereader', function($q) {
    var slice = Array.prototype.slice;

    return {
        restrict: 'A',
        require: '?ngModel',
        link: function(scope, element, attrs, ngModel) {
                if (!ngModel) return;

                ngModel.$render = function() {};

                element.bind('change', function(e) {
                    var element = e.target;

                    $q.all(slice.call(element.files, 0).map(readFile))
                        .then(function(values) {
                            if (element.multiple) ngModel.$setViewValue(values);
                            else ngModel.$setViewValue(values.length ? values[0] : null);
                        });

                    function readFile(file) {
                        var deferred = $q.defer();

                        var reader = new FileReader();
                        reader.onload = function(e) {
                            deferred.resolve(e.target.result);
                        };
                        reader.onerror = function(e) {
                            deferred.reject(e);
                        };
                        reader.readAsDataURL(file);

                        return deferred.promise;
                    }

                }); //change

            } //link
    }; //return
});

and invoke it like this:

<input type="file" ng-model="editItem._attachments_uri.image" accept="image/*" app-filereader />

The property (editItem.editItem._attachments_uri.image) will be populated with the contents of the file you select as a data-uri (!).

Please do note that this script will not upload anything. It will only populate your model with the contents of your file encoded ad a data-uri (base64).

Check out a working demo here: http://plnkr.co/CMiHKv2BEidM9SShm9Vv

Elmer
  • 8,198
  • 1
  • 41
  • 34
  • 4
    Look promising, can you please explain the logic behind the code, and comment about browser compatibility(IE and non - fileAPI browser mostly)? – Oleg Belousov Nov 06 '13 at 18:05
  • Also, to the best of my understanding, if I will set the content-type header of the AJAX request to undefined, and will try to send such a field to the server, angular will upload it, assuming that the browser supports fileAPI, am I correct? – Oleg Belousov Nov 06 '13 at 18:06
  • @OlegTikhonov you are not correct! This script will not send anything. It will read the file you selected as a Base64 string and update your model with that string. – Elmer Nov 15 '13 at 07:33
  • @Elmer Yes, I understand, what I mean is that by sending a form that contains a file field (a relative path to the file in the user's machine in a FileAPI object), you can make the angular upload the file by an XHR request by setting the content type header to undefined – Oleg Belousov Nov 15 '13 at 20:16
  • Hi @Elmer it' was still not working, but the issue was the ng-repeat. Changing `image in dummy.images` to `image in dummy.images track by $index` worked just fine now. thanks – Diego Vieira Jan 10 '14 at 16:39
  • 1
    What is the purpose of overwritting `ngModel`'s `$render` function? – sp00m Apr 06 '16 at 12:21
  • Is it possible to get the filename with this directive? The upload works great but I need the filenames of the selected images, too. Could somebody help? – m1crdy May 03 '16 at 16:12
  • How can `restrict: 'A'`? – Anders Lindén Nov 08 '16 at 10:32
  • The `reader.readAsDataURL` method is obsolete. Modern code uses [URL.create​ObjectURL()](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL). – georgeawg May 12 '19 at 23:23
46

How to enable <input type="file"> to work with ng-model

Working Demo of Directive that Works with ng-model

The core ng-model directive does not work with <input type="file"> out of the box.

This custom directive enables ng-model and has the added benefit of enabling the ng-change, ng-required, and ng-form directives to work with <input type="file">.

angular.module("app",[]);

angular.module("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);
      })
    }
  }
});
<script src="//unpkg.com/angular/angular.js"></script>
  <body ng-app="app">
    <h1>AngularJS Input `type=file` Demo</h1>
    
    <input type="file" select-ng-files ng-model="fileArray" multiple>

    <code><table ng-show="fileArray.length">
    <tr><td>Name</td><td>Date</td><td>Size</td><td>Type</td><tr>
    <tr ng-repeat="file in fileArray">
      <td>{{file.name}}</td>
      <td>{{file.lastModified | date  : 'MMMdd,yyyy'}}</td>
      <td>{{file.size}}</td>
      <td>{{file.type}}</td>
    </tr>
    </table></code>
    
  </body>
Community
  • 1
  • 1
georgeawg
  • 46,994
  • 13
  • 63
  • 85
  • You can use condition to check if there is no selected files, ng-model be undefined **** if(files.length > 0) { ngModel.$setViewValue(files); } else { ngModel.$setViewValue(undefined); } – Farshad Kazemi Jun 28 '17 at 09:00
  • How to get data of the file ? And what are the other attributes we can use like {{file.name}} – Adarsh Singh May 23 '19 at 13:34
  • 1
    @AdarshSingh See [MDN Web API Reference - File](https://developer.mozilla.org/en-US/docs/Web/API/File) and [MDN Reference - Using files from web applications](https://developer.mozilla.org/en-US/docs/Web/API/File/Using_files_from_web_applications) – georgeawg May 23 '19 at 13:42
26

This is an addendum to @endy-tjahjono's solution.

I ended up not being able to get the value of uploadme from the scope. Even though uploadme in the HTML was visibly updated by the directive, I could still not access its value by $scope.uploadme. I was able to set its value from the scope, though. Mysterious, right..?

As it turned out, a child scope was created by the directive, and the child scope had its own uploadme.

The solution was to use an object rather than a primitive to hold the value of uploadme.

In the controller I have:

$scope.uploadme = {};
$scope.uploadme.src = "";

and in the HTML:

 <input type="file" fileread="uploadme.src"/>
 <input type="text" ng-model="uploadme.src"/>

There are no changes to the directive.

Now, it all works like expected. I can grab the value of uploadme.src from my controller using $scope.uploadme.

Per Quested Aronsson
  • 9,570
  • 8
  • 47
  • 70
9

I create a directive and registered on bower.

This lib will help you modeling input file, not only return file data but also file dataurl or base 64.

{
    "lastModified": 1438583972000,
    "lastModifiedDate": "2015-08-03T06:39:32.000Z",
    "name": "gitignore_global.txt",
    "size": 236,
    "type": "text/plain",
    "data": "data:text/plain;base64,DQojaWdub3JlIHRodW1ibmFpbHMgY3JlYXRlZCBieSB3aW5kb3dz…xoDQoqLmJhaw0KKi5jYWNoZQ0KKi5pbGsNCioubG9nDQoqLmRsbA0KKi5saWINCiouc2JyDQo="
}

https://github.com/mistralworks/ng-file-model/

peterh
  • 9,698
  • 15
  • 68
  • 87
yozawiratama
  • 3,663
  • 11
  • 50
  • 83
4

This is a slightly modified version that lets you specify the name of the attribute in the scope, just as you would do with ng-model, usage:

    <myUpload key="file"></myUpload>

Directive:

.directive('myUpload', function() {
    return {
        link: function postLink(scope, element, attrs) {
            element.find("input").bind("change", function(changeEvent) {                        
                var reader = new FileReader();
                reader.onload = function(loadEvent) {
                    scope.$apply(function() {
                        scope[attrs.key] = loadEvent.target.result;                                
                    });
                }
                if (typeof(changeEvent.target.files[0]) === 'object') {
                    reader.readAsDataURL(changeEvent.target.files[0]);
                };
            });

        },
        controller: 'FileUploadCtrl',
        template:
                '<span class="btn btn-success fileinput-button">' +
                '<i class="glyphicon glyphicon-plus"></i>' +
                '<span>Replace Image</span>' +
                '<input type="file" accept="image/*" name="files[]" multiple="">' +
                '</span>',
        restrict: 'E'

    };
});
asiop
  • 551
  • 2
  • 11
3

function filesModelDirective(){
  return {
    controller: function($parse, $element, $attrs, $scope){
      var exp = $parse($attrs.filesModel);
      $element.on('change', function(){
        exp.assign($scope, this.files[0]);
        $scope.$apply();
      });
    }
  };
}
app.directive('filesModel', filesModelDirective);
georgeawg
  • 46,994
  • 13
  • 63
  • 85
coder000001
  • 464
  • 3
  • 21
  • 1
    Kudos for returning the [file](https://developer.mozilla.org/en-US/docs/Web/API/File) object. The other directives that convert it to a DataURL make it difficult for controllers that want to upload the file. – georgeawg May 12 '19 at 23:29
3

For multiple files input using lodash or underscore:

.directive("fileread", [function () {
    return {
        scope: {
            fileread: "="
        },
        link: function (scope, element, attributes) {
            element.bind("change", function (changeEvent) {
                return _.map(changeEvent.target.files, function(file){
                  scope.fileread = [];
                  var reader = new FileReader();
                  reader.onload = function (loadEvent) {
                      scope.$apply(function () {
                          scope.fileread.push(loadEvent.target.result);
                      });
                  }
                  reader.readAsDataURL(file);
                });
            });
        }
    }
}]);
Uelb
  • 3,367
  • 2
  • 17
  • 29
2

I had to do same on multiple input, so i updated @Endy Tjahjono method. It returns an array containing all readed files.

  .directive("fileread", function () {
    return {
      scope: {
        fileread: "="
      },
      link: function (scope, element, attributes) {
        element.bind("change", function (changeEvent) {
          var readers = [] ,
              files = changeEvent.target.files ,
              datas = [] ;
          for ( var i = 0 ; i < files.length ; i++ ) {
            readers[ i ] = new FileReader();
            readers[ i ].onload = function (loadEvent) {
              datas.push( loadEvent.target.result );
              if ( datas.length === files.length ){
                scope.$apply(function () {
                  scope.fileread = datas;
                });
              }
            }
            readers[ i ].readAsDataURL( files[i] );
          }
        });

      }
    }
  });
Hugo
  • 71
  • 2
1

I had to modify Endy's directive so that I can get Last Modified, lastModifiedDate, name, size, type, and data as well as be able to get an array of files. For those of you that needed these extra features, here you go.

UPDATE: I found a bug where if you select the file(s) and then go to select again but cancel instead, the files are never deselected like it appears. So I updated my code to fix that.

 .directive("fileread", function () {
        return {
            scope: {
                fileread: "="
            },
            link: function (scope, element, attributes) {
                element.bind("change", function (changeEvent) {
                    var readers = [] ,
                        files = changeEvent.target.files ,
                        datas = [] ;
                    if(!files.length){
                        scope.$apply(function () {
                            scope.fileread = [];
                        });
                        return;
                    }
                    for ( var i = 0 ; i < files.length ; i++ ) {
                        readers[ i ] = new FileReader();
                        readers[ i ].index = i;
                        readers[ i ].onload = function (loadEvent) {
                            var index = loadEvent.target.index;
                            datas.push({
                                lastModified: files[index].lastModified,
                                lastModifiedDate: files[index].lastModifiedDate,
                                name: files[index].name,
                                size: files[index].size,
                                type: files[index].type,
                                data: loadEvent.target.result
                            });
                            if ( datas.length === files.length ){
                                scope.$apply(function () {
                                    scope.fileread = datas;
                                });
                            }
                        };
                        readers[ i ].readAsDataURL( files[i] );
                    }
                });

            }
        }
    });
Parley Hammon
  • 27
  • 1
  • 3
1

If you want something a little more elegant/integrated, you can use a decorator to extend the input directive with support for type=file. The main caveat to keep in mind is that this method will not work in IE9 since IE9 didn't implement the File API. Using JavaScript to upload binary data regardless of type via XHR is simply not possible natively in IE9 or earlier (use of ActiveXObject to access the local filesystem doesn't count as using ActiveX is just asking for security troubles).

This exact method also requires AngularJS 1.4.x or later, but you may be able to adapt this to use $provide.decorator rather than angular.Module.decorator - I wrote this gist to demonstrate how to do it while conforming to John Papa's AngularJS style guide:

(function() {
    'use strict';

    /**
    * @ngdoc input
    * @name input[file]
    *
    * @description
    * Adds very basic support for ngModel to `input[type=file]` fields.
    *
    * Requires AngularJS 1.4.x or later. Does not support Internet Explorer 9 - the browser's
    * implementation of `HTMLInputElement` must have a `files` property for file inputs.
    *
    * @param {string} ngModel
    *  Assignable AngularJS expression to data-bind to. The data-bound object will be an instance
    *  of {@link https://developer.mozilla.org/en-US/docs/Web/API/FileList `FileList`}.
    * @param {string=} name Property name of the form under which the control is published.
    * @param {string=} ngChange
    *  AngularJS expression to be executed when input changes due to user interaction with the
    *  input element.
    */
    angular
        .module('yourModuleNameHere')
        .decorator('inputDirective', myInputFileDecorator);

    myInputFileDecorator.$inject = ['$delegate', '$browser', '$sniffer', '$filter', '$parse'];
    function myInputFileDecorator($delegate, $browser, $sniffer, $filter, $parse) {
        var inputDirective = $delegate[0],
            preLink = inputDirective.link.pre;

        inputDirective.link.pre = function (scope, element, attr, ctrl) {
            if (ctrl[0]) {
                if (angular.lowercase(attr.type) === 'file') {
                    fileInputType(
                        scope, element, attr, ctrl[0], $sniffer, $browser, $filter, $parse);
                } else {
                    preLink.apply(this, arguments);
                }
            }
        };

        return $delegate;
    }

    function fileInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
        element.on('change', function (ev) {
            if (angular.isDefined(element[0].files)) {
                ctrl.$setViewValue(element[0].files, ev && ev.type);
            }
        })

        ctrl.$isEmpty = function (value) {
            return !value || value.length === 0;
        };
    }
})();

Why wasn't this done in the first place? AngularJS support is intended to reach only as far back as IE9. If you disagree with this decision and think they should have just put this in anyway, then jump the wagon to Angular 2+ because better modern support is literally why Angular 2 exists.

The issue is (as was mentioned before) that without the file api support doing this properly is unfeasible for the core given our baseline being IE9 and polyfilling this stuff is out of the question for core.

Additionally trying to handle this input in a way that is not cross-browser compatible only makes it harder for 3rd party solutions, which now have to fight/disable/workaround the core solution.

...

I'm going to close this just as we closed #1236. Angular 2 is being build to support modern browsers and with that file support will easily available.

p0lar_bear
  • 2,004
  • 2
  • 17
  • 30
0

Alternatively you could get the input and set the onchange function:

<input type="file" id="myFileInput" />
document.getElementById("myFileInput").onchange = function (event) {
   console.log(event.target.files);                        
};
Ricardo Valente
  • 318
  • 1
  • 6
  • 10
-2

Try this,this is working for me in angular JS

    let fileToUpload = `${documentLocation}/${documentType}.pdf`;
    let absoluteFilePath = path.resolve(__dirname, fileToUpload);
    console.log(`Uploading document ${absoluteFilePath}`);
    element.all(by.css("input[type='file']")).sendKeys(absoluteFilePath);
RRR
  • 75
  • 1
  • 11