Простая смесь JavaScript и AngularJS, не отображающая содержимое файла из HTML <input type="file"

Я видел примеры, которые используют директивы, позволяющие AngularJS получать доступ к содержимому или свойствам файла (например, в Alex Such fiddle и сообщение в блоге), но я бы подумал, что следующий простой код будет работать (это не так).

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 изменяется с "[Ожидание файла]" и заменяется содержимым выбранного файла txt (в моем случае "Hallo" Мир"). Точка останова на controller.showFileContent(event.target.result) показывает то же самое, значение изменяется от "[Ожидание файла]" до "Hallo World".

Но HTML никогда не обновляется, он остается как "[Ожидание файла]". Почему?

(N.B. Я поставил код в скрипку.)

Ответы

Ответ 1

Код в ответе deitch выглядит правильно и, безусловно, помог мне понять вещи. Но AngularJS не поддерживает привязку ввода файлов, поэтому ng-change не будет работать. Глядя на обсуждение в что проблема GitHub, похоже, связано с тем, что привязка к вводу файла HTML и его загрузка воспринимаются как синонимы и потому что реализация API полного файла более сложный. Однако существуют чисто локальные сценарии, в которых мы хотим загрузить входные данные из локального файла в модель Angular.

Мы могли бы получить это с помощью директивы Angular как Alex. Это делает в fiddle и сообщение в блоге Я упоминаю в вопросе или как hon2a отредактированный ответ и laurent answer do.

Но для достижения этого без директив нам необходимо обработать входной файл onchange вне контекста Angular, но затем использовать 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]);
    }
};

Ответ 2

Основная концепция событий вне Angular правильная, но у вас есть 2 места, которые вы выходите за пределы контекста Angular:

  • onchange="grabFileContent()" заставляет все grabFileContent() запускаться за пределами контекста Angular
  • 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]);
        }
    };
});

Ответ 3

При прослушивании событий вне среды AngularJS (таких как события DOM или событие FileReader) вам необходимо обернуть код слушателя в вызове $apply(), чтобы правильно запустить a $digest и впоследствии обновить представление.

fileReader.addEventListener('load', function (event) {
    $scope.$apply(function () {
        // ... (put your code here)
    });
});

Вам нужно будет каким-то образом передать область своей функции.


Кроме того, как указывает deitch 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".

Ответ 4

Я написал обертку вокруг API FileReader, который вы можете найти здесь

Он в основном обертывает прототипы 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, которая позволяет привязывать к файлу свойство модели. Конечно, привязка - это только один способ, поскольку входной элемент не позволяет установить файл (по соображениям безопасности)