Angular.jsで、アップロードするファイルをcontrollerでも使えるようdirectiveを自作する

| コメントをどうぞ

ちまたにはAngular.jsのファイルアップロードのモジュールがたくさんあるのですが、シンプルで単機能なものが欲しく、自作しました。
ファイルの内容が、

<input type="file"> → directiveのchangeイベント → controller

の順で渡るところのコードです。
controllerでその後サーバーにアップロードするところは省きます。

前提のコード

下記のように、ファイルをアップロードするコードがあるとします。
index.htmlの中で、upload.htmlをincludeしています。
upload.htmlの中ではmy-sectionというdirectiveをtranscludeで使い、またtype=”file”のinputタグがあります。

index.html

<html>
    <body>
        <div ng-controller="MainController">
                     <div ng-include="'upload.html'"></div>
        </div>
    </body>
</html>

upload.html

<my-section>
  <form>
    <div class="fileinput-container">
        <input type="file" id="file-input" my-file-input>
    </div>
    <div class="button-container">
    <button id="upload-file" ng-click="upload()" type="button">
        <span>ファイルのアップロード</span>
    </button>
    </div>
  </form>
<my-section>            

directives.js

app.directive('mySection', [function(){
    return {
        templateUrl: 'section.html',//<div class="row"><div ng-transclude></div></div>
        transclude: true
    }
}]);

controllers.js

app.controller('MainController', ['$scope', function($scope){
        $scope.upload = function(){
       var file = $scope.xxx;//ここでどうにかしてアップロードしたファイルを取得したい
       //サーバーへファイルをアップロードするコードが続く
           ...
    }
    }]);

ファイルの中身を取得するためにはDOMにアクセスしなくてはならないので、directiveを利用する必要があります。
directiveで取得したファイルを、どうやってcontrollerで利用できるようにするか、について検証しました。

方法1: デフォルトscopeにセットする

下記のページを参考にしました。
1. AngularJSでファイルをアップロードする
2. Multipart/form-data File Upload with AngularJS
3. 2のjsfiddle

upload.html
myFileInputディレクティブを属性で設定。そこで利用する変数名を値に指定します。

...
<input type="file" id="file-input" my-file-input="targetFile">
...

directives.js
attrs.myFileInputは、targetFile。targetFileという変数に、element[0].files[0]で取得したファイルの中身をセットしています。

app.directive('myFileInput', ['$parse', function ($parse) {
    return {
    link: function(scope, element, attrs) {
        var model = $parse(attrs.myFileInput);
        var modelSetter = model.assign;
        element.bind('change', function(){
            scope.$apply(function(){
                modelSetter(scope.$parent.$parent, element[0].files[0]);
            });
        });
    }
    };
}]);

$parseやassignやら使っていますが、そこは重要ではありません。
注目はこの部分。

 modelSetter(scope.$parent.$parent, element[0].files[0]);

scope.$parent.$parentスコープのtargetFile変数に、ファイルの中身であるelement[0].files[0]をセットしています。
参考にしたサイトでは、directiveでscopeにセット出来ていましたが、私が書いたコードの場合は、directiveをネストしていたため、scopeがコントローラと異なってしまい、$parentで辿らなくてはならなくなりました。
つまり、余談ですが、directiveで取れるスコープは、下記のようになっていました。

scope

directiveごとにscopeの階層が深くなるのかと思いきや、一概にそうとも言えず。なかなか仕様の奥が深いです、Angular.js。templateプロパティがあるかないか、transcludeかどうか、includeされているかどうか、あたりが関係してそう。
とにかくこの実装の問題点は、
directiveを多用してscopeの階層が深くなると、$parentを多用しなくてはならなくなる
ということです。
できることなら$parentで参照はしたくないので、この方法は、directiveがネストする場合には微妙です。

方法2: 継承スコープを利用する

継承スコープは、javascriptのプロトタイプによる継承を利用したスコープです。
controllerのスコープに継承を経由してアクセスすることができます。

directives.js

app.directive('myFileInput', ['$parse', function ($parse) {
    return {
        scope: true,//継承スコープを使う
    link: function(scope, element, attrs) {
        element.bind('change', function(){
             //controllerのスコープのメソッドに継承を利用してアクセス
                        scope.setTargetFile(element[0].files[0]);
        });
    }
    };
}]);

controllers.js

app.controller('MainController', ['$scope', function($scope){
    //input type=fileで選択したら、一度スコープの変数にセット 
  $scope.setTargetFile = function(file){
        $scope.targetFile = file;
    }
    $scope.upload = function(){
        var file = $scope.targetFile;
        //サーバーへファイルをアップロードするコードが続く
        ...
    }
    }]);

これでもいいですが、気になるのはcontrollerのsetTargetFileのコードが冗長だということです。
setTargetFileなどどいうメソッドを用意しなくても、targetFile変数にセットされるようにしたい。

方法3: 独立したスコープで双方向データバインディング

最後はdirectiveで独立したスコープを用意し、controllerのスコープの変数と双方向バインディングさせる方法です。
この方法だと、方法2のようにわざわざメソッドを用意しなくてもよいです。

upload.html

<my-section>
  <form>
    <div class="fileinput-container">
        <!--混乱を避けるため、directive(my-file-input)と
          双方向バインディングの指定(data="...")は別にしておく。
        -->
        <input type="file" id="fileInput" my-file-input data="targetFile">
    </div>
    <div class="button-container">
    <button id="upload-file" ng-click="upload()" type="button">
        <span>ファイルのアップロード</span>
    </button>
    </div>
  </form>
<my-section>            

directives.js

app.directive('myFileInput', [function () {
    return {
        scope:{data: "="},//dataという属性に指定した値を、controllerで双方向バインディングさせる
        link: function(scope, element, attrs) {
            element.bind('change', function(){
                scope.$apply(function(){
                    scope.data.src = element[0].files[0];
                });
            });
        }
    };
}]);

controllers.js

app.controller('MainController', ['$scope', function($scope){
    $scope.upload = function(){
        //directiveのscope.data.srcにセットした値は、controllerで$scope.targetFile.srcとして利用できる
        var file = $scope.targetFile.src;
        //サーバーへファイルをアップロードするコードが続く
        ...
    }

    //最初に、アップロードファイルを格納する変数を用意しておく←ポイント
    $scope.targetFile = {
    src:''
    };
}]);

最初にアップロードファイルを格納する変数をcontrollerで用意しておくのがポイントです。
これがないとdirectiveで変数がundefinedになってしまいます。

この三番目の方法が一番スマートかと思います。

参考URL

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

次のHTML タグと属性が使えます: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>