Пишем свой Uploader с нуля на javascript используя FileApi. Часть5. +AngularJS

В этой части хочу рассказать о том, как можно все эти операции с файлами завернуть в AngularJS.

Рекомендую пролистать предыдущие части перед началом разбора этой:

Разберем несколько ключевых поментов подключения FileApi к AngularJS:

  • проблема ng-model и input file
  • сервис для FileApi
  • превью директива

Проблема ng-model

Проблема заключается в том, что ng-model не работает с input-file, то есть он не обновит связанную модель, когда через компонент буду выбраны файлы.

Более подробно о проблеме можно почитать тут.

Приходиться писать свое решение – директиву расширяющую возможности ng-model для данного элемента. Что должна делать директива? Обновлять модель по событию change (это родное браузерное событие, которое нормально отрабатывает с input-file), то есть:

directive('fileChanged', function() {
    return {
      restrict: 'A',
      link: function($scope, element) {

        element.bind('change', function(event) {
          //...
        });
      }
    };
  });

теперь нам нужно подгрузить модель, для этого используем свойство директивы require:

directive('fileChanged', function() {
    return {
      restrict: 'A',
      require: '?ngModel',
      link: function($scope, element, attrs, ngModel) {
        if (!ngModel) {
          return;
        }

        element.bind('change', function(event) {
            //...
        });
      }
    };
  });

Используем “?” при загрузке, чтобы избежать ошибки(иметь возможность самим обработать) при отсутствии ng-model директивы на элементе.
Зададим значение модели используя метод $setViewValue и обновим скоуп используя $scope.$apply:

  directive('fileChanged', function() {
    return {
      restrict: 'A',
      require: '?ngModel',
      link: function($scope, element, attrs, ngModel) {
        if (!ngModel) {
          return;
        }

        element.bind('change', function(event) {
          ngModel.$setViewValue(event.target.files[0]);
          $scope.$apply();
        });
      }
    };

И последний штрих – переопределим метод $render для ngModel на angular.noop, чтобы, когда мы обновляли значение модели, он не пытался ничего обновить во вью(мы сами контролируем этот процесс):

directive('fileChanged', function() {
    return {
      restrict: 'A',
      require: '?ngModel',
      link: function($scope, element, attrs, ngModel) {
        if (!ngModel) {
          return;
        }

        ngModel.$render = angular.noop;

        element.bind('change', function(event) {
          ngModel.$setViewValue(event.target.files[0]);
          $scope.$apply();
        });
      }
    };
  });

Пример полностью.

Сервис для FileApi

Так как работаем с AngularJS, то использовать FileReader напрямую не комильфо: необходимо создать AngularJS-сервис, который будет оберткой над window.FileReader:

factory('FileReader', function($window) {

    if (!$window.FileReader) {
      throw new Error('Browser does not support FileReader');
    }

    function readAsDataUrl(file) {
      var reader = new $window.FileReader();

      reader.onload = function() {
        //...
      };

      reader.onerror = function() {
        //...
      };

      reader.readAsDataURL(file);

      return reader;
    }

    return {
      readAsDataUrl: readAsDataUrl
    };
}

как-то так. Обязательно через $window, а не window, как минимум для того чтобы потом удобнее было покрывать юнит-тестами.

Так как операции работы с файлами у нас асинхронные, то без промисов нам не обойтись – добавляем $q:

factory('FileReader', function($q, $window) {

if (!$window.FileReader) {
throw new Error('Browser does not support FileReader');
}

function readAsDataUrl(file) {
  var deferred = $q.defer(),
  reader = new $window.FileReader();

  reader.onload = function() {
    deferred.resolve(reader.result);
  };

  reader.onerror = function() {
    deferred.reject(reader.error);
  };

  reader.readAsDataURL(file);

  return deferred.promise;
}

return {
  readAsDataUrl: readAsDataUrl
};
}

Код.

Превью директива

Ну и в заключение для того, чтобы использовать только что написаный сервис FileReader сделаем директиву, которая будет отображать превью загруженной картинки. HTML-представление будет выглядеть где-то так:

<input type="file" ng-model="newImage" file-changed />
<img file-preview="newImage" />
  • file-changed – ранее описанная директива-фикс ng-model
  • file-preview – наша новая директива, которая будет отвечать за отобразжение превью при выборе файла через модель newImage

а код директивы:

directive('filePreview', function (FileReader) {
    return {
        restrict: 'A',
        scope: {
            filePreview: '='
        },
        link: function (scope, element, attrs) {
            scope.$watch('filePreview', function (filePreview) {
                if (filePreview && Object.keys(filePreview).length !== 0) {
                    FileReader.readAsDataUrl(filePreview).then(function (result) {
                        element.attr('src', result);
                    });
                }
            });
        }
    };
});

FileReader – раннее созданный сервис оболочка на window.FileReader с промисами
filePreview: ‘=’ – создаем изолированный скоуп и линкуем модель
scope.$watch(‘filePreview’ – отслеживаем изменение модели
FileReader.readAsDataUrl(filePreview) – считываем файл
element.attr(‘src’, result) – задаем картинку

Поиграться с примером можно тут.