AngularJSでファイルをアップロードする


AngularJSでファイルをアップロードしたい。画像の場合はアップロード前にプレビューもしたい（追記：さらにドラッグアンドドロップにも対応）。
私の書き方も悪いのでしょうけど、Angularだから特に楽ってことはないな。まあ、素直に[ng-file-upload](https://github.com/danialfarid/ng-file-upload)使えばいいのだろうけど。

###前提知識

* \<input type="file">のファイル（名）は標準ディレクティブでは取得できない。
* FormDataを利用して、かつ、multipart/form-dataで送る。

つまり、カスタムディレクティブを定義する必要がある。

###HTML（フロント）

ソースは少々長いが、やっていることは至ってシンプル。要所はカスタムディレクティブのところ。

* ディレクティブ名はキャメル形式が-とかに変わる（hogeFooはhoge-fooというディレクティブ名になる）
* DOMからscopeへの値の紐付けのタイミングと内容は、element.bindで指定する（逆はwatch?）
* FormDataを利用し、transformRequestはnull（何もしない）、Content-typeはundefinedに（multipart/formになる）

ここでは、ディレクティブにfile-modelとつけ、linkにて、変数名$scope.fileで操作できるようにする。そして、changeイベント発生の際、$scope.fileにelement[0].files[0]の値を関連付けている。
いろいろ？？？なところもあるが、いわゆるおまじないとして覚えた方が早い。

```html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
<script src="js/app.js"></script>
<script>
//app
var app = angular.module('myApp',[]);
//controller
app.controller('hogeCtrl',function($scope,$http){

	//init
	$scope.name = "";
	$scope.email = "";
	$scope.file = "";

	//onclick
	$scope.btnClick = function(){

		//formdata
		var fd = new FormData();
		fd.append('name',$scope.name);
		fd.append('email',$scope.email);
		fd.append('file',$scope.file);

		//post
		$http.post('res.php',fd,{
			transformRequest: null,
			headers: {'Content-type':undefined}
		})
		.success(function(res){
			$scope.response = res;
		});
	}

});

//directive
app.directive('fileModel',function($parse){
	return{
		restrict: 'A',
		link: function(scope,element,attrs){
			var model = $parse(attrs.fileModel);
			element.bind('change',function(){
				scope.$apply(function(){
					model.assign(scope,element[0].files[0]);
				});
			});
		}
	};
});
</script>
</head>
<body>
<div ng-app="myApp" ng-controller="hogeCtrl">

name:<input type="text" name="name" ng-model="name"><br>
email:<input type="text" name="email" ng-model="email"><br>
file:<input type="file" file-model="file"><br>
<button ng-click="btnClick()">upload</button>

<p>{{name}}</p>
<p>{{response}}</p>

</div>
</body>
</html>
```

###サーバ側（PHP）

ここでは、各種パラメータが受け取れているかどうかだけチェック。
受け取り方とかは、[ここ](http://qiita.com/zaburo/items/80b94bce19a4a670e465)を参考に。

```php
<?php

	$name = $_POST['name'];
	$email = $_POST['email'];
	$file = $_FILES['file']['name'];

	echo $name." ".$email." ".$file;
```

###応用（プレビューする）

せっかくangularを使うので、アップロード前にファイルが画像ならプレビューもできるようにしたい。
基本、上記のコードに、少し追加するだけである。主なポイントは、

* \<input type="file">の変化をwatch
* 変化があり、かつ画像ファイルならHTML5 File APIを利用して画像を読み込む
* 読みこむ先の\<img>を追加（ng-ifで表示、非表示をコントロール）

という感じ。$scope.$watchと\<img>のところだけ追加。

```html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
<script src="js/app.js"></script>
<script>
//app
var app = angular.module('myApp',[]);
//controller
app.controller('hogeCtrl',function($scope,$http){

	//init
	$scope.name = "";
	$scope.email = "";
	$scope.file = "";

	//onclick
	$scope.btnClick = function(){

		//formdata
		var fd = new FormData();
		fd.append('name',$scope.name);
		fd.append('email',$scope.email);
		fd.append('file',$scope.file);

		//post
		$http.post('res.php',fd,{
			transformRequest: null,
			headers: {'Content-type':undefined}
		})
		.success(function(res){
			$scope.response = res;
		});
	}

	//変化を監視して画像読み込み＋表示を実行
	$scope.$watch("file",function(file){

		$scope.srcUrl = undefined;

		//画像ファイルじゃなければ何もしない
		if(!file || !file.type.match("image.*")){
			return;
		}

		//new FileReader API
		var reader = new FileReader();

		//callback
		reader.onload = function(){
			$scope.$apply(function(){
				$scope.srcUrl = reader.result;
			});
		};

		//read as url(reader.result = url)
		reader.readAsDataURL(file)
	});

});

//directive
app.directive('fileModel',function($parse){
	return{
		restrict: 'A',
		link: function(scope,element,attrs){
			var model = $parse(attrs.fileModel);
			element.bind('change',function(){
				scope.$apply(function(){
					model.assign(scope,element[0].files[0]);
				});
			});
		}
	};
});
</script>
</head>
<body>
<div ng-app="myApp" ng-controller="hogeCtrl">

name:<input type="text" name="name" ng-model="name"><br>
email:<input type="text" name="email" ng-model="email"><br>
file:<input type="file" file-model="file"><br>
<img ng-if="srcUrl" ng-src="{{srcUrl}}"><br>
<button ng-click="btnClick()">upload</button>

<p>{{srcUrl}}</p>
<p>{{response}}</p>

</div>
</body>
</html>
```


商品情報や人の情報など、テキストデータと画像ファイルを紐付けてアップロード（登録）するような状況は多いが、上記を覚えておけば汎用的に使える。

上記コードではfile.sizeでファイル容量が取得できるので、アップロード前に容量をチェックすることもできる。

###さらに応用（ドラッグアンドドロップ）

折角なのでドラッグアンドドロップにも対応

主な変更としては、カスタムディレクティブを追加。ドロップしたイベントでファイルをロード。プレビューは上記のロジックを転用。
FormDataに$scope.fileを直接代入していたが、$scope.upfileを用意して、ファイル選択かどラックランドドロップのどちらかから入力してアップロード。空ならエラーを出す。あとは、dragenterとかdragleaveの時のCSSの変更くらい。

```html
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
<script>
//app
var app = angular.module('myApp',[]);
//controller
app.controller('hogeCtrl',function($scope,$http){

    //init
    $scope.name = "";
    $scope.email = "";
    $scope.file = "";

    //upfile
    $scope.upfile = "";

    //onclick
    $scope.btnClick = function(){

        if($scope.upfile == "" || $scope.upfile == undefined){
            alert("ファイルが選択されていません");
            return;
        }

        //formdata
        var fd = new FormData();
        fd.append('name',$scope.name);
        fd.append('email',$scope.email);
        fd.append('file',$scope.upfile);

        //post
        $http.post('res.php',fd,{
            transformRequest: null,
            headers: {'Content-type':undefined}
        })
        .success(function(res){
            $scope.response = res;
        });
    }

    //変化を監視して画像読み込み＋表示を実行
    $scope.$watch("file",function(file){

        $scope.srcUrl = undefined;

        //画像ファイルじゃなければ何もしない
        if(!file || !file.type.match("image.*")){
            return;
        }

        //new FileReader API
        var reader = new FileReader();

        //callback
        reader.onload = function(){
            $scope.$apply(function(){
                $scope.srcUrl = reader.result;
                $scope.upfile = file;
            });
        };

        //read as url(reader.result = url)
        reader.readAsDataURL(file)
    });

    $scope.addNewFile = function(dropFile){

        //
        $scope.srcUrl = undefined;

        //
        var file = dropFile[0];
        var reader = new FileReader();


        reader.onload = function(){
            $scope.$apply(function(){
                console.log(file.name);
                $scope.srcUrl = reader.result;
                $scope.upfile = file;
            });
        }

        reader.readAsDataURL(file);
    }

});

//ファイル処理
app.directive('fileModel',function($parse){
    return{
        restrict: 'A',
        link: function(scope,element,attrs){
            var model = $parse(attrs.fileModel);
            element.bind('change',function(){
                scope.$apply(function(){
                    model.assign(scope,element[0].files[0]);
                });
            });
        }
    };
});

//ドラッグアンドドロップ
app.directive('fileDropZone',function(){
    return{
        restrict: 'A',
        scope:{onDropFile: '&'},
        link: function(scope,element,attrs){

            //when dragover & enter
            var processDragOverOrEnter = function(event){
                event.stopPropagation();
                event.preventDefault();
                //背景色変更
                element.css('background-color','#aaa');
            }

            //when drop
            var processDrop = function(event){
                event.stopPropagation();
                event.preventDefault();
                element.css('background-color',"#fff");

                scope.onDropFile({file:event.dataTransfer.files});
                
            }

            var processDragLeave = function(event){
                //背景色戻す
                element.css('background-color',"#fff");
            }

            //bind event to function
            element.bind('dragover',processDragOverOrEnter);
            element.bind('dragenter',processDragOverOrEnter);
            element.bind('drop',processDrop);
            element.bind('dragleave',processDragLeave);
        }
    }
});
</script>
<style>
#zone{
    border: 2px #aaa dotted;
    padding: 20px;
    width: 300px;
    text-align: center;
}

#zone:-webkit-drag-over{
    background-color: #aaa;
}
</style>
</head>
<body>
<div ng-app="myApp" ng-controller="hogeCtrl">

name:<input type="text" name="name" ng-model="name"><br>
email:<input type="text" name="email" ng-model="email"><br>
file:<input type="file" file-model="file"><br>
<div id="zone" file-drop-zone on-drop-file="addNewFile(file)">Drop file here.</div>
<img ng-if="srcUrl" ng-src="{{srcUrl}}"><br>
<button ng-click="btnClick()">upload</button>

<p>{{srcUrl}}</p>
<p>{{response}}</p>

</div>
</body>
</html>
```

###メモ

・プログレスを表示したいが、$httpじゃできないらしい。