用简单的 JavaScript 和 AngularJS 的混合代码无法在HTML的<input type="file">中显示文件内容

4
我可以帮您翻译成中文。这段内容是关于编程的,讲解了如何使用指令使AngularJS能够访问文件的内容或属性。在Alex Such的fiddleblog post中有相关示例。但是下面这段简单的代码并不能实现这一功能。
HTML:
<body ng-app="myapp">
    <div id="ContainingDiv" ng-controller="MainController as ctrl">
        <input id="uploadInput" type="file" name="myFiles" onchange="grabFileContent()" />
        <br />
        {{ ctrl.content }}
    </div>
</body>

JavaScript:

var myapp = angular.module('myapp', []);

myapp.controller('MainController', function () {
    this.content = "[Waiting for File]";
    this.showFileContent = function(fileContent){
        this.content = fileContent;
    };
});

var grabFileContent = function() {
    var files = document.getElementById("uploadInput").files;
    if (files && files.length > 0) {
        var fileReader = new FileReader();
        fileReader.addEventListener("load", function(event) {
            var controller = angular.element(document.getElementById('ContainingDiv')).scope().ctrl;
            controller.showFileContent(event.target.result);
        });
        fileReader.readAsText(files[0]);
    }
};

如果我在 this.content = fileContent 这一行上设置断点,我可以看到 content 的值从 “[Waiting for File]” 改变为所选文本文件的内容(在我的例子中是 "Hallo World")。在 controller.showFileContent(event.target.result) 上设置断点也可以看到相同的情况,值从 "[Waiting for File]" 变为 "Hallo World"。

但是 HTML 永远不会重新渲染,它仍然是“[Waiting for File]”。为什么呢?
(注:我已经把代码放在了a fiddle 中。)

我尝试了从“this”更换为“$scope”,但HTML仍无法呈现更新(修订后的fiddle)。 - dumbledad
4个回答

4

事件在Angular之外的主要概念是正确的,但有两个地方超出了Angular上下文:

  1. onchange="grabFileContent()" 会导致所有的 grabFileContent() 在Angular上下文之外运行。
  2. fileReader.addEventListener('load', function(event){ ... 会导致回调在Angular上下文之外运行。

以下是我会如何做。首先,将onchange事件移到Angular上下文和控制器中:

<input id="uploadInput" type="file" name="myFiles" ng-model="ctrl.filename" ng-change="grabFileContent()" />

现在从您的控制器中:

myapp.controller('MainController', function ($scope) {
    this.content = "[Waiting for File]";
    this.showFileContent = function(fileContent){
        this.content = fileContent;
    };
    this.grabFileContent = function() {
        var that = this, files = this.myFiles; // all on the scope now
        if (files && files.length > 0) {
            var fileReader = new FileReader();
            fileReader.addEventListener("load", function(event) {
                // this will still run outside of the Angular context, so we need to 
                // use $scope.$apply(), but still...
                // much simpler now that we have the context for the controller
                $scope.$apply(function(){
                    that.showFileContent(event.target.result);
                });
            });
            fileReader.readAsText(files[0]);
        }
    };
});

谢谢@deitch。这很有趣,但它能工作吗?我已经将您的代码添加到a fiddle中,但HTML不会随文件内容更新。 - dumbledad
在调试器中查看,this.grabFileContent从未被调用,所以似乎ng-change无法成功将事件路由到处理程序。这似乎是一个已解决的AngularJS问题,但已关闭。奇怪的是,他们似乎认为处理文件输入与文件上传是同义词,这可能是为什么他们没有实现它的原因。 - dumbledad
所以,如果我正确地认为这是一个已知的遗漏,AngularJS不会从文件输入HTML控件路由事件,那么我现在有**工作代码**,它将事件处理程序保持在Angular上下文之外,就像我的问题一样,但使用您的技术在控制器的showFileContent方法中使用$scope.$apply() - dumbledad
哎呀!我错过了 ng-change 的问题。很好的发现 @dumbledad - deitch

1
在监听AngularJS环境外的事件(例如DOM事件或FileReader事件)时,您需要将监听器代码包装在$apply()调用中,以正确触发$digest并随后更新视图。
fileReader.addEventListener('load', function (event) {
    $scope.$apply(function () {
        // ... (put your code here)
    });
});

你需要想办法将作用域传递给你的函数。

此外,正如deitch's answer所指出的那样,您不应该使用原生事件处理程序属性,例如onchange,而应该使用Angular方法,例如ng-change。在文件输入的情况下,这种方法将不起作用,您最好创建一个指令来捕获本机change事件,并使用文件内容更新您的作用域变量:

.directive('fileContent', ['$parse', function ($parse) {
    return {
        compile: function (element, attrs) {
            var getModel = $parse(attrs.fileContent);

            return function link ($scope, $element, $attrs) {
                $element.on('change', function () {
                    var reader = new FileReader();
                    reader.addEventListener('load', function (event) {
                        $scope.$apply(function () {
                            // ... (get file content)
                            getModel($scope).assign(/* put file content here */);
                        });
                    });
                    // ... (read file content)
                });
            };
        }
    };
}])

&

<input type="file" file-content="someScopeVariable">

这将自动使用当前选择的文件的内容更新您的作用域变量。它展示了“以Angular思考”的典型关注点分离。

我现在意识到AngularJS不支持文件输入绑定,因此ng-change无法使用。尽管如此,我很想错了;你有使用ng-change而不是onchange的可行文件输入代码示例吗? - dumbledad
抱歉,我没有意识到这一点。在这种情况下,我建议将整个内容包装在指令中,在其中捕获“change”事件,并使用$apply()来包装处理程序代码。我会尝试更新我的答案以展示我的意思。 - hon2a

1

这段代码在deitch的答案中看起来没问题,确实帮助我理解了一些东西。但是AngularJS不支持文件输入绑定,所以ng-change不能使用。从GitHub问题讨论中可以看出,这是因为将HTML的文件输入绑定和上传文件视为同义词,并且实现完整的文件API更加复杂。但是,有一些纯本地场景需要将输入从本地文件加载到Angular模型中。

我们可以像Alex Such在问题中提到的fiddle博客文章一样,使用Angular指令来实现此目的,或者像hon2a的编辑答案laurent的答案一样。
但是,要在没有指令的情况下实现此目标,我们需要在Angular上下文之外处理文件输入的onchange事件,然后使用Angular的$scope.$apply()方法来通知Angular结果的更改。 这里是一些代码可以实现这一点
HTML:
<div id="ContainingDiv" ng-controller="MainController as ctrl">
    <input id="uploadInput" type="file" name="myFiles" onchange="grabFileContent()" />
    <br />
    {{ ctrl.content }}
</div>

JavaScript:

var myApp = angular.module('myApp',[]);

myApp.controller('MainController', ['$scope', function ($scope) {
    this.content = "[Waiting for File]";
    this.showFileContent = function(fileContent){
        $scope.$apply((function(controller){
            return function(){
                controller.content = fileContent;
            };
        })(this));
    };
}]);

var grabFileContent = function() {
    var files = document.getElementById("uploadInput").files;
    if (files && files.length > 0) {
        var fileReader = new FileReader();
        fileReader.addEventListener("load", function(event) {
           var controller = angular.element(document.getElementById('ContainingDiv')).scope().ctrl;
           controller.showFileContent(event.target.result);
        });
        fileReader.readAsText(files[0]);
    }
};

对我来说,Alex Such在fiddle上的代码有效。你的答案给了我一个“引用错误:grabFileContent未定义”的错误,但无论如何还是谢谢你。 - Andre Rocha

1

我写了一个围绕 FileReader API 的包装器,你可以在 这里 找到它。

它基本上将 FileReader 原型方法包装在 Promise 中,并确保事件处理程序在 $apply 函数内调用,以便与 Angular 的桥接完成。

有一个快速示例,介绍如何从指令中使用它来显示图像预览。

(function (ng) {
'use strict';
ng.module('app', ['lrFileReader'])
    .controller('mainCtrl', ['$scope', 'lrFileReader', function mainCtrl($scope, lrFileReader) {
        $scope.$watch('file', function (newValue, oldValue) {
            if (newValue !== oldValue) {
                lrFileReader(newValue[0])
                    .on('progress', function (event) {
                        $scope.progress = (event.loaded / event.total) * 100;
                        console.log($scope.progress);
                    })
                    .on('error', function (event) {
                        console.error(event);
                    })
                    .readAsDataURL()
                    .then(function (result) {
                        $scope.image = result;
                    });
            }
        });
    }])
    .directive('inputFile', function () {
        return{
            restrict: 'A',
            require: 'ngModel',
            link: function linkFunction(scope, element, attrs, ctrl) {


                //view->model
                element.bind('change', function (evt) {
                    evt = evt.originalEvent || evt;
                    scope.$apply(function () {
                        ctrl.$setViewValue(evt.target.files);
                    });
                });

                //model->view
                ctrl.$render = function () {
                    //does not support two way binding
                };
            }
        };
    });
})(angular);

请注意inputFile指令,它允许将文件绑定到模型属性。当然,绑定只是单向的,因为输入元素不允许设置文件(出于安全原因)。

感谢@laurent。我试图避免使用指令(我在问题中提到了Alex Such的解决方案),因为感觉不使用指令也应该很简单,但看到如何使用它们还是很好的。 - dumbledad

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