Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Simple mixture of JavaScript and AngularJS not displaying file contents from HTML <input type="file"

Tags:

angularjs

I have seen examples that use directives to enable AngularJS to access the content or properties of a file (for example in Alex Such's fiddle and blog post) but I would have thought the following simple code would work (it doesn't).

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

If I place a breakpoint on the line this.content = fileContent I can see that the value of content changes from "[Waiting for File]" and is replaced by the content of the chosen txt file (in my case "Hallo World"). A breakpoint on controller.showFileContent(event.target.result) shows the same, the value changes from "[Waiting for File]" to "Hallo World".

But the HTML never re-renders, it stays as "[Waiting for File]". Why?

(N.B. I've put the code in a fiddle.)

like image 613
dumbledad Avatar asked Mar 18 '23 15:03

dumbledad


2 Answers

The main concept of events outside Angular is correct, but you have 2 places you are going outside of the Angular context:

  1. onchange="grabFileContent()" causes all of grabFileContent() to be run outside of the Angular context
  2. fileReader.addEventListener('load', function(event){ ... causes the callback to be run outside of the Angular context

Here is how I would do it. First, move the onchange event into the angular context and the controller:

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

And now from within your controller:

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]);
        }
    };
});
like image 93
deitch Avatar answered Apr 25 '23 07:04

deitch


I wrote a wrapper around the FileReader API, which you can find here

It basically wraps FileReader prototype methods in Promise and make sure the event handlers are called within an $apply function so the bridge with Angular is done.

There is a quick example on how to use it from a directive to display preview of image

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

Note the inputFile directive which allow the binding with a File to a model property. Of course the binding is only one way as the input element does not allow to set the file (for security reason)

like image 36
laurent Avatar answered Apr 25 '23 07:04

laurent