使用指令DEMO的ng-model与`<input type="file"/>`相关联

292

我试图在 type 为 file 的 input 标签上使用 ng-model:

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

但是在选择文件后,在控制器中,$scope.vm.uploadme仍然未定义。

我该如何在控制器中获取所选文件?


3
请参考 https://dev59.com/hmMm5IYBdhLWcg3wDbin#17923521,特别是在 http://jsfiddle.net/danielzen/utp7j/ 上引用的在线示例。 - Daryn
我相信在使用ngModel时,您始终需要在HTML元素上指定name属性。 - Sam
13个回答

334

我使用指令创建了一个解决方法:

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

然后输入标签变为:

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

或者只需要文件定义:

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

4
我在img标签中使用uploadme作为src,这样我就可以看到指令正在设置它。但是,如果我尝试使用$scope.uploadme从控制器中提取它,它会显示“未定义”。不过我可以从控制器中设置uploadme。例如,$scope.uploadme="*"会使图像消失。 - Per Quested Aronsson
5
问题在于该指令创建了一个子作用域,并在该作用域中设置了“uploadme”。原始(父级)作用域也有一个“uploadme”,并且不受子作用域的影响。我可以从任何一个作用域更新HTML中的“uploadme”。有没有办法完全避免创建子作用域? - Per Quested Aronsson
4
@AlexC,问题是关于ng-model无法工作,而不是上传文件的:) 那时我不需要上传文件。但最近我通过这个egghead.io教程学会了如何上传文件。请您知悉。 - Endy Tjahjono
11
不要忘记 $scope.$on('$destory', function(){ element.unbind("change"); }。 - Nawlbergs
3
我有一个问题...与纯JavaScript和HTML相比,这不是过于复杂了吗?说真的,你真的需要理解AngularJS才能达到这个解决方案...而且似乎我可以用JavaScript事件做同样的事情。为什么要按照Angular的方式而不是纯JS的方式来做呢? - AFP_555
显示剩余15条评论

53

我使用这个指令:

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

并像这样调用它:

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

属性(editItem.editItem._attachments_uri.image)将会被选定文件的内容以data-uri(base64编码)的形式填充。

请注意,此脚本不会上传任何内容。它只会将您的文件内容作为数据uri(base64编码)填充到模型中。

在这里查看一个工作示例: http://plnkr.co/CMiHKv2BEidM9SShm9Vv


4
看起来很有前途,请您解释一下代码的逻辑,并评论浏览器兼容性(主要是 IE 和不支持 FileAPI 的浏览器)? - Oleg Belousov
另外,据我所知,如果我将 AJAX 请求的 content-type 标头设置为 undefined,并尝试将这样的字段发送到服务器,Angular 将上传它,假设浏览器支持 fileAPI,我是正确的吗? - Oleg Belousov
@OlegTikhonov,您说得不对!这个脚本不会发送任何东西。它将以Base64字符串的形式读取您选择的文件,并使用该字符串更新您的模型。 - Elmer
@Elmer 是的,我明白,我的意思是通过发送包含文件字段(FileAPI对象中用户机器上文件的相对路径)的表单,您可以通过将内容类型标头设置为未定义来使angular上传文件通过XHR请求。 - Oleg Belousov
1
重写 ngModel$render 函数的目的是什么? - sp00m
显示剩余4条评论

48

如何使<input type="file">ng-model一起使用

ng-model一起工作的指令演示

核心指令ng-model默认不支持<input type="file">

这个自定义指令可以启用ng-model,并且还能让ng-changeng-requiredng-form指令与<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>


您可以使用条件语句来检查是否没有选择文件,ng-model 为未定义 **** if(files.length > 0) { ngModel.$setViewValue(files); } else { ngModel.$setViewValue(undefined); } - Farshad Kazemi
如何获取文件的数据?还有哪些其他属性可以使用,例如{{file.name}}? - Adarsh Singh

26
这是对@endy-tjahjono的解决方案的补充。
我最终无法从作用域中获取uploadme的值。尽管指令明显更新了HTML中的uploadme,但我仍然无法通过$scope.uploadme访问它的值。但我可以从作用域中设置它的值。神秘,不是吗..?
事实证明,指令创建了一个子作用域,并且子作用域有自己的uploadme
解决方案是使用对象而不是原始类型来保存uploadme的值。
在控制器中,我有:
$scope.uploadme = {};
$scope.uploadme.src = "";

在HTML中:

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

指令没有任何更改。

现在,一切都像预期的那样工作。我可以使用 $scope.uploadme 从我的控制器中获取 uploadme.src 的值。


1
是的,就是这样。 - Per Quested Aronsson
1
我确认有同样的经验;非常好的调试和解释。我不确定为什么指令会创建自己的作用域。 - Adam Casey
或者,一种内联声明方式:$scope.uploadme = { src: '' } - maxathousand

9
我创建了一个指令并在 bower 上注册。
这个库将帮助您建立输入文件的模型,不仅返回文件数据,还会返回数据 URL 或 Base64 格式的文件数据。
{
    "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/


如何使用$scope?我尝试使用它,但在调试时得到了未定义的结果。 - Gujarat Santana
1
干得好yozawiratama!它运行良好。如果是<input type="file" ng-file-model="myDocument"/>,那么只需使用$scope.myDocument.name或一般性地使用$scope.myDocument.<任何属性>,其中属性为["lastModified", "lastModifiedDate", "name", "size", "type", "data"]之一,@GujaratSantana。 - Jay Dharmendra Solanki
无法通过 Bower 安装。 - Pimgd
如何使用多文件上传? - aldrien.h
reader.readAsDataURL 方法已经过时。现代代码使用 URL.create​ObjectURL() - georgeawg

4
这是一个稍微修改过的版本,让你可以像使用ng-model一样指定作用域中属性的名称。用法:
    <myUpload key="file"></myUpload>

指令:

.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'

    };
});

3

使用lodash或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);
                });
            });
        }
    }
}]);

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

1
感谢您返回file对象。将其转换为DataURL的其他指令会使想要上传文件的控制器难以处理。 - georgeawg

2

我需要在多个输入上执行相同的操作,因此我更新了@Endy Tjahjono的方法。它返回一个包含所有已读文件的数组。

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

      }
    }
  });

1
我不得不修改Endy的指令,以便我可以获得最后修改时间、最后修改日期、名称、大小、类型和数据,同时能够获得文件数组。对于那些需要这些额外功能的人,这里提供给你们。
更新: 我发现一个bug,如果你选择文件然后再次去选择但是取消,文件似乎永远不会被取消选择。所以我更新了我的代码来解决这个问题。
 .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] );
                    }
                });

            }
        }
    });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接