AngularJS Advent Calendar 2015 24日目の投稿です。
知人からどんな感じで Angular を書いてるのか尋ねられたので、ボクはこうしてますよという投稿です。良い例か悪い例かはボク自身判断できないのですが、参考になればと。
とりあえずコード全部出す
ちょっと長いですが、全部出します。内容はただのTodoでRESTfulに通信してます。一覧をクリックすると編集でき、blur のタイミングで保存しにいくようにしてあります。ファイルはめんどうなのでわけてません。
(function(){
'use strinoct';
angular.module('MyApp', ['ngResource'])
.controller('SampleTodoCtrl', ['$scope', 'SampleTodoService', function($scope, SampleTodoService){
var vm = $scope.vm = {};
function init(){
SampleTodoService.getTodo(vm);
}
vm.clickAddBtn = function(){
SampleTodoService.addTodo(vm);
};
vm.toEdit = function(index){
vm.todoList[index].edit = true;
};
vm.editDone = function(todo){
todo.edit = false;
SampleTodoService.editTodo(vm, todo);
};
vm.clickDeleteBtn = function(index){
SampleTodoService.deleteTodo(vm, index);
};
init();
}])
.factory('SampleTodoService', ['SampleTodoApi', function(SampleTodoApi){
var factory = {};
var api = SampleTodoApi.getApi();
factory.getTodo = function(vm){
api.get(function(result){
vm.todoList = result.todoList || [];
})
};
factory.addTodo = function(vm){
api.save(vm.todo, function(){
vm.todoList.push(angular.copy(vm.todo));
vm.todo = '';
});
};
factory.editTodo = function(vm, editTodo){
api.save(editTodo);
};
factory.deleteTodo = function(vm, index){
var deleteTodo = vm.todoList[index];
api.delete(deleteTodo.id, function(){
vm.todoList.splice(index, 1);
});
};
return factory;
}])
.factory('SampleTodoApi', ['$resource', function($resource){
var factory = {};
factory.getApi = function(){
return $resource('/api/todo');
};
return factory;
}])
})();
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.0-rc.0/angular.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.5.0-rc.0/angular-resource.js""></script>
<script src="app.js"></script>
<title>code sample</title>
</head>
<body ng-app="MyApp">
<div ng-controller="SampleTodoCtrl">
<div ng-form="sampleForm">
<input type="text" name="todo" ng-model="vm.todo.title">
<button ng-click="vm.clickAddBtn()">Add</button>
<ul>
<li ng-repeat="todo in vm.todoList" ng-form="sampleListForm">
<div>
<span ng-if="!todo.edit" ng-click="vm.toEdit($index)">{{todo.title}}</span>
<span ng-if="todo.edit"><input type="text" name="editTodo" ng-model="todo.title" ng-blur="vm.editDone(todo)"></span>
<button ng-click="vm.clickDeleteBtn($index)">削除</button>
</div>
</li>
</ul>
</div>
</div>
</body>
</html>
結果こうなりました
$scope 直下にプリミティブ型のプロパティを生やさない
これはよくいわれているので、対応している人も多いはず。スコープの継承で、親子スコープ間でデータ共有できなくなります。
そのため、初期化時には
var vm = $scope.vm = {};
とします。
また、コントローラ内で $scope
を書くのめんどうなので、ショートカットもつくっておきます。
コントローラの役割を厳格にする
昔はコントローラの引数に複数のサービスを読んで組み合わせていたんですけど、どうしても「サービス1の結果に対して分岐処理をする」みたいになってしまって、結果ロジック書いちゃってるじゃん!ってなってました。コントローラを薄くするためにもコントローラの役割を以下のように決めてしまいました。
コントローラは、
- スコープの初期化
- ビューからのイベントを補足して、サービスの該当処理を呼び出す
- サービス(非同期通信など)で取得したデータをビューに渡す
のみ。それ以外はさせないようにしてます(フラグ値をかえすくらいならコントローラでやってもいいかなとは思ってます)。
vm.clickAddBtn = function(){
SampleTodoService.addTodo(vm);
};
要は、追加ボタンが押されたら、Todoを追加する処理してね、ってだけです。
サービスはコントローラに対して 1:N
コントローラにロジックを書けなくなったので、サービスに書かざるを得ません。そのためコントローラから複数サービスを呼ぶのではなく、そのコントローラに対応するサービスを1つだけDIするようにして、そのサービス内で複数のサービスを組み合わせようにします。
.factory('SampleTodoService', ['SampleTodoApi', 'ここに', '並ぶ', function(SampleTodoApi, a, b){
var factory = {};
...
この例だと SampleTodoApi
だけですが、紹介している方法で書いていくと、この箇所でDIするサービスがたくさん並ぶことになります。
1:N の理由ですが、管理画面などの、入力/確認/完了/一覧 は共通的な機能が多いためです。
それぞれの画面に対して 1:1 で作るよりは、コードを使い回せるので楽できます。ただし、あまりにも1ファイルのサイズが大きくなった場合は 1:1 で作ったほうが良いです。
vm をサービスに渡してしまう
これが一番のポイントかもしれません。通常はこうしたくなるはず。
vm.clickAddBtn = function(){
// ここでパラメータ作る
var param = ...
vm.todoList = SampleTodoService.addTodo(param); // パラメータ渡す
};
こうすると、サービスのためにパラメータを加工して渡すことが多くなります。
ただ、それすらコントローラに書きたくないという意地!
で、考えた結果、 vm
ごと渡してしまいます。パラメータは渡した先で作るようにします。そして、コントローラでビューのデータをセットするのではなく、サービスに vm
を渡してセットしてもらいます。
初期化の際に、vm = $socpe.vm
しているため、双方向データバインディングされた状態です。
なので、サービスでセットしたデータはビューに反映されます。
「最小限のネストしたオブジェクト渡せばいいじゃん」、という方もいると思います、そう思います。
ただ、後から別のプロパティを参照しないといけなくなった場合、変更がめんどうなのでまるっと渡してしまう方針にしました。
で、結果これが良いな感じている今日この頃です。
失敗作として、
.controller('SampleTodoCtrl', ['$scope', 'SampleTodoService', function($scope){
$scope.vm = {};
SampleTodoService.setViewModel($scope.vm);
...
}]).
.factory('SampleTodoService', ['SampleTodoApi', function(SampleTodoApi){
var factory = {};
var vm = {};
factory.setViewModel = function(viewModel){
vm = viewModel;
};
...
のように、ビューモデル用の setter
をつくろうとしたこともありました(サービスで保持させれば毎回 vm
を渡さなくて良いので)。Angularの場合、サービスは singleton
なので、毎回コントローラの初期化時にセットする必要性が生じてしまいます。忘れてしまって前回使用されたデータを参照してしまった、などのバグの温床になると考えやめました。
まとめ
Angularユーザのみなさんはどんな感じで書いてますか?
せっかく晒したのでレビュープリーズ。