morningstar
morningstar

Reputation: 9122

How do I two-way bind to a type="file" input with ng-model?

Basically I want to do this:

<input type="file" ng-model="variable_in_scope">

When I pick a file, variable_in_scope should get assigned to the file object picked. Also, if the value of variable_in_scope gets changed anywhere else in my page, it should update the text next to the "Choose File" button to indicate that the chosen file has changed.

With any other type of input, this would just work.

I don't need to do anything fancy like actually see the contents of the file. Ultimately, I want to post it, but I've found out you can do this by setting the file object you get into a FormData object, without actually reading the contents into Javascript-land.

I've found other questions about picking files with angular, but none had a two-way binding solution.

Upvotes: 3

Views: 6551

Answers (3)

ED8500
ED8500

Reputation: 51

Helped for me: FileUploader

Hide the input element with display: none; then use a label to go on top of the input element, and a label you connect to your scope variable. See numerous posts on SO. Like this:

<label for="idFileUpload" class="custom-file-upload">
    SelectFile
</label>
<input type="file" id="idFileUpload" nv-file-select uploader="uploader">
<label id="idSelectedFile">{{selectedFile}}</label>
<md-button id="idUploadBtn" md-no-ink class="md-primary settingsBtns" ng-click="uploader.uploadAll()" ng-disabled="!uploader.getNotUploadedItems().length">Upload</md-button>

CSS:

input[type="file"] {
    display: none;
}

.custom-file-upload {
    background-color: green;
    color: white;
    border: 1px solid #ccc;
    display: inline-block;
    padding: 6px 12px;
    cursor: pointer;
}

In the events the FileUploader fires, you can fetch the value of the input element. HTH

Upvotes: 0

morningstar
morningstar

Reputation: 9122

Angular doesn't support binding to file-type inputs, but I cobbled together a solution using a number of other answers.

app.directive('filePicker', filePicker);

filePicker.$inject = ['$log', '$document'];

function filePicker($log,$document) {

  var directive = {
    restrict: 'A',
    require: 'ngModel',
    scope: {
      ngModel: '='
    },
    link: _link
  };

  return directive;

  function _link(scope, elem, attrs, ngModel) {

    // check if valid input element
    if( elem[0].nodeName.toLowerCase() !== 'input' ) {
      $log.warn('filePicker:', 'The directive will work only for input element, actual element is a', elem[0].nodeName.toLowerCase());
      return;
    }

    // check if valid input type file
    if( attrs.type != 'file' ) {
      $log.warn('filePicker:', 'Expected input type file, received instead:', attrs.type, 'on element:', elem);
      return;
    }

    // listen for input change
    elem.on('change', function(e) {

      // get files
      var files = elem[0].files;

      // update model value
      scope.$apply(function() {
          attrs.multiple ? scope.ngModel = files : scope.ngModel = files[0];
      });
    });

    scope.$watch('ngModel', function() {
        if (!scope.ngModel)
            elem[0].value = ""; // clears all files; there's no way to remove only some
    });

  }

}

This solution showed me how to use a directive to implement a custom binding to ng-model. It enables accessing the contents of the file, so if you need that functionality you can add it back to my solution.

However, it had some problems with its binding. It would correctly set the value of my variable_in_scope, but if there were other things bound to the value of variable_in_scope, they wouldn't update. The trick was to use isolate scope and $apply. Then you don't need to mess with this $setViewValue business. Just set it and forget it.

That got me as far as one-way-binding. If I set a value to variable_in_scope, however, the file picker still showed that I had the original file selected. In my case all I really want to do is clear the selected file. I found out the Javascript magic to do this and set up a $watch on the ngModel to trigger it.

If you want to set the file to a different value programmatically, good luck to you, because FileList is read-only. The magic trick lets you clear the FileList, but you can't add anything back. Maybe you can create a new FileList and assign it to .files, but at a cursory glance I didn't see a way to do that.

Upvotes: 0

JLRishe
JLRishe

Reputation: 101652

My answer on another question provides a way to do this with ng-model, but since that question is not specifically about two way binding (and my answer is fairly hard to find there), I'll reproduce it here:

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

To use it, you simply need to add this to your angular module and include a bind-file attribute on the file pickers where you want to use it.

Upvotes: 3

Related Questions