AngularJS + jQuery + jQueryUI(ダイアログ) を試してみた

なんとなくTodoアプリを作ってみました。
実際に動くものは ここ で確認できます。

独自ディレクティブについて。
リファクタリングすれば多少はきれいになるとは思いますが、
凝ったことをやろうと思うと結構考えないと、すぐにぐちゃぐちゃになりそうですね。
あと、サービスと同様な感じで分離させれないかなぁ考えています。
今回みたいにcompileでスコープ取得したい場合の公式なやり方が見つけれなくて
独自に実装した(_target変数使ってるところ)んですけど、標準的なやり方ってどんなんでしょう?

地味に詰まったのが、ラジオボタンのON/OFF設定。attr()じゃなくてprop()を使うんですね。。


テンプレート

見た目まんまhtmlです。これがAngularJSによってコンパイルされ、ビューとなります。
独自ディレクティブを作成する必要はありますが、jQueryを使うこともできます。今回はjQueryUIのダイアログを使用しています。

<!DOCTYPE html>
<html>
<head>
	<meta charset="utf-8">
	<title>タスク管理</title>
	<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-2.1.0.min.js"></script>
	<script src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.10.3/jquery-ui.min.js"></script>
	<link rel="stylesheet" href="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.10.3/themes/smoothness/jquery-ui.css" />
	<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular.min.js"></script>
	<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.14/angular-route.min.js"></script>
	<script src="http://underscorejs.org/underscore-min.js"></script>
	<script src="services.js"></script>
	<script src="app.js"></script>
	<style>
		li:hover{
			background : #CEF2F5;
			cursor : pointer;
		}
		.todo-item-finished{
			color : green;
		}
		.todo-item-working{
			color : red;
		}
	</style>
</head>
<body ng-app="app">
	<div ng-controller="todoCtrl">
		<form name="form1">
			<p>【新規登録】</p>
			<input type="text" name="newTitle" ng-model="newTitle" ng-init="" required></input>
			<button ng-click="addNewTask()" ng-disabled="form1.$invalid">追加</button>
		</form>
		<div style="margin-top:50px;">
			<p>【タスク一覧】</p>
			<ul style="padding-left:5px;list-style-type:none;">
				<li ng-repeat="item in items" style="hover: background:red;">
					<p todo-editable class="todo-item-{{item.finished ? 'finished' : 'working'}}">{{item.id}} - {{item.title}} / {{item.finished ? '完了' : '未完了'}}</p>
				</li>
			</ul>
		</div>
	</div>
	<div id="dialog-form" title="タスクの編集">
		<p><input type="text" name="title" /></p>
		<p><input type="radio" name="status" id="task-finished" value="true">完了</input>
		<input type="radio" name="status" id="task-working" value="false">未完了</input></p>
	</div>
</body>
</html>

サービス

var services = {};

(function(){
	/**
	* Todoサービス
	*/
	services.TodoService = (function(){
		var Todo = function(){
			this._items = [
				{id : 1, title : "タスク1", finished : false},
				{id : 2, title : "タスク2", finished : true},
				{id : 3, title : "タスク3", finished : false},
				{id : 4, title : "タスク4", finished : true},
				{id : 5, title : "タスク5", finished : false}
			];
		};

		Todo.prototype.getAll = function(){
			return this._items;
		};

		var _add = function(title){
			this._items.push({
				id : this._items.length+1,
				title : title,
				finished : false
			});
		};
		
		var _update = function(item){
			var updateTgt = _.find(this._items, function(i){ return i.id === item.id;});
			if(updateTgt === undefined){
				_add.call(this, item.title);
			}else{
				updateTgt.title = item.title;
				updateTgt.finished = item.finished;
			}
		};

		Todo.prototype.update = function(item){
			_.isString(item) ? _add.call(this, item) : _update.call(this, item);
		};

		Todo.prototype.delete = function(item){
			//todo:実装
		};
		
		return Todo;
	})();
	
	
	/**
	* 非同期Todoサービス
	*/
	services.AsyncTodoService = (function(){
		
		//memo:定義場所について。
		//このサービスはAngularJSの$timeoutサービスに依存している。
		//ファクトリの登録関数内で定義するのが正解かもしれない。
		
		var AsyncTodo = function($timeout){
			this.$timeout = $timeout || function(action){action()};
			this.todo = new services.TodoService();
		};
		
		AsyncTodo.prototype.getAll = function(){
			return this.todo.getAll();
		};

		var _applyCurrying = function(me, fnc){
			return function(){
				return fnc.apply(me, arguments);
			}
		};

		AsyncTodo.prototype.update = function(item){
			var updAction = _applyCurrying(this.todo, this.todo.update);
			this.$timeout(function(){
				updAction(item);
			}, 1000);
		};

		AsyncTodo.prototype.delete = function(item){
			var delAction = _applyCurrying(this.todo, this.todo.delete);
			this.$timeout(function(){
				delAction(item);
			}, 2000);
		};
		
		return AsyncTodo;
	})();
	
})();

コントローラーと独自ディレクティブ

todoCtrlコントローラーとサービス登録、todoEditableディレクティブの定義を行います。

(function(){
	//AngularJSの1.2以降はngRouteのインストール?を明示的に指定する必要がある。
	//http://docs.angularjs.org/error/$injector/modulerr?p0=app&p1=Error:%20%5B$injector:nomod
	var ngtodoApp = angular.module("app",["ngRoute"]);

	ngtodoApp.factory("Todo", ["$timeout", function($timeout){
		return new services.AsyncTodoService($timeout);
	}]);
	
	ngtodoApp.controller("todoCtrl", ["$scope", "Todo", function($scope, todo){
		$scope.items = todo.getAll();
		$scope.addNewTask = function(){
			todo.update($scope.newTitle);
			alert("登録しました。");
			$scope.newTitle = "";
		};
	}]);
	
	//以下は http://angularjsninja.com/blog/2013/11/22/angularjs-custom-directives/ より抜粋
	//
	//■compile と link の使い分け
	//compileの function はng-repeatの繰り返しに関係なく一度だけ呼び出されるのに対し、linkの function はイテレーションのたびに呼び出されることになる。
	//compileとlinkの両方を指定した場合にはlinkが無視される仕様になっているため、両方を利用した実装をしたい場合にはcompilefunction からlinkfunction を return するよう実装することになる。
	//
	//
	//■いろいろある link の書き方
	//単にfunctionを return するだけという書き方ができて、これはlinkプロパティだけを持つオブジェクトを返しているのと同じことになる。link以外のプロパティを指定する必要が無ければ、こう書くことでシンプルなコードにできる。
	//
	//  ngtodoApp.directive("listRow", function(){
	//  	return function(scope, element, attrs){
	//  		...
	//  	};
	//  });
	//
	ngtodoApp.directive("todoEditable", ["Todo", function(todo){
		
		var _target = (function(){
			
			function Target(){
				this.scope = {};
			}
			
			Target.prototype.clear = function(){
				this.scope = undefined;
				this.scope = { $index : -1 };  //ダミースコープオブジェクトのセット
			};
			
			var tgtObj = new Target();
			tgtObj.clear();
			return tgtObj;
		})();
		
		return {
			restrict: 'A',
			compile : function(element, attrs){
				var okClick = function(){
					var dialog = $("#dialog-form");
					var title = $("input[name='title']",dialog).val();
					var finished = $("input[type='radio']:eq(0)",dialog).prop("checked");
					
					if(_.isEmpty(title)){
						alert("タイトルが未入力です");
						return;
					}
					
					todo.update({
						id : _target.scope.item.id,
						title : title,
						finished : finished
					});
					
					$(this).dialog("close");
					_target.clear();
				};
				
				var cancelClick = function(){
					$(this).dialog("close");
					_target.clear();
				};
				
				$("#dialog-form").dialog({
					autoOpen: false,
					height: 280,
					width: 350,
					modal: true,
					buttons: [
						{
							text: "OK",
							click : okClick
						},
						{
							text: "Cancel",
							click : cancelClick
						}
					]
				});
				
				return function(scope, element, attrs){
					$(element).click(function(){
						_target.scope = scope;
						var dialog = $("#dialog-form");
						$("input[name='title']", dialog).val(scope.item.title);
						
						if(scope.item.finished)
							$("#task-finished", dialog).prop("checked", true);
						else
							$("#task-working", dialog).prop("checked", true);
						
						dialog.dialog("open");
					});
				};
			}
		};
	}]);
})();