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");
					});
				};
			}
		};
	}]);
})();

2階層以上のプロトタイプチェーンについて

「prototypeオブジェクトにベースオブジェクトインスタンスを設定したら、多重継承ってどうやるんだ??ベースクラスを設定したらサブクラスのprototype書き換えられないじゃん???」
という考えから抜け出せられず、また継承使わずとも十分実装できることもあり、
prototypeとは深くかかわらないようにしてたのですが、今回ようやくスッキリすることができました。


確認用コード

var BaseHoge = (function(){
    function BaseHoge() {
      this.hoge = "ベースオブジェクト";
    }
  BaseHoge.prototype.getHoge = function(){
      return this.hoge;
  };
  BaseHoge.prototype.setHoge = function(val){
    this.hoge = val;
  };
  return BaseHoge;
})();

var SubFoo = (function(){
  function SubFoo() {
      this.foo = "サブオブジェクト"; 
  }
  SubFoo.prototype = new BaseHoge.prototype.constructor();
  SubFoo.prototype.constructor = SubFoo;
  SubFoo.prototype.getFoo = function(){
      return this.foo;
  };
  
  return SubFoo;
})();

var SubSubBar = (function(){
  function SubSubBar() {
      this.bar = "サブサブオブジェクト"; 
  }
  
  //BaseHogeインスタンスオブジェクトを
  //prototypeプロパティにセットする。下記構成になる。
  //※コードの通り、prototypeオブジェクトの中身が
  //SubFooインスタンスオブジェクトになるだけ。
  //
  //  prototype {
  //    foo : "サブオブジェクト",
  //    constructor : SubFoo,
  //    __proto__ : SubFoo.prototype
  //  }
  //
  SubSubBar.prototype = new SubFoo.prototype.constructor();
  
  //コンストラクタプロパティを時オブジェクトに書き換える
  //これを行わないと、外からオブジェクトタイプを調べたりすることができなくなってしまう。
  //変更後は、下記構成になる。
  //
  //  prototype {
  //    foo : "サブオブジェクト",
  //    constructor : SubSubBar,
  //    __proto__ : SubFoo.prototype
  //  }
  //
  SubSubBar.prototype.constructor = SubSubBar;
  
  //自オブジェクトに持たせる関数の定義
  SubSubBar.prototype.getBar = function(){
      return this.bar;
  };
  
  //最終的にはこんな感じ。
  //constructorが指す関数と、暗黙リンク(__proto__)が
  //指す関数が異なる構成だけど、問題なく動作する。
  //prototypeオブジェクトも他オブジェクトとなんら変わりない普通の
  //オブジェクトで、プロトタイプチェーンに必要なプロパティを持っていれば
  //どんなオブジェクトでも動作する。
  //
  //  prototype {
  //    foo : "サブオブジェクト",
  //    getFoo : function(){ ... },
  //    constructor : SubSubBar,
  //    __proto__ : SubFoo.prototype
  //  }
  //
  return SubSubBar;
})();

console.clear();

//インスタンスオブジェクトの中身はこんな感じ
//{
//  bar : "サブサブオブジェクト",
//  prototype : {
//    foo : "サブオブジェクト"
//    getFoo : function(){ ... },
//    constructor : SubSubBar,
//    __proto__ : SubFoo.prototype
//  },
//  constructor : SubSubBar,
//  __proto__ : SubSubBar.prototype
//}
//
var obj = new SubSubBar();
console.dir(obj);

//obj.__proto__.__proto__.__proto__.setHoge()を実行する。
//実行後のオブジェクトの中身は以下な感じ。
//
//{
//  bar : "サブサブオブジェクト",
//  hoge : "ほげ~",
//  prototype : { ... }
//  constructor : SubSubBar,
//  __proto__ : SubSubBar.prototype
//}
//
obj.setHoge("ほげ~");

//メソッド場所の検索、処理フローはsetHoge()と同様。
console.log("getHoge() : ", obj.getHoge());

Edit fiddle - JSFiddle

参考書籍

開眼!  JavaScript ―言語仕様から学ぶJavaScriptの本質

開眼! JavaScript ―言語仕様から学ぶJavaScriptの本質

これに全部載ってます。

カスタムオブジェクトの定義について

JavaScriptのprototype周りを勉強しながら、過去・現在の自分で書いたコードを見直したところ、
ちょっとまずい構成をしていたことが判明しました。例として以下のコードで考えます。

//ストップウオッチ
var StopWatch = function(){
	var _st = undefined;
	var _ed = undefined;

	var _start = function(){
		_st = new Date();
	}
	var _stop = function(){
		_ed = new Date();
	}
	var _clear = function(){
		_st = undefined;
		_ed = undefined;
	}
	var _getElapsedMilliseconds = function(){
		if(_st === undefined || _st === undefined)
			return -1;
		return _ed.getTime() - _st.getTime();
	}
	
	return {
		start : _start,
		stop : _stop,
		clear : _clear,
		getElapsedMilliseconds : _getElapsedMilliseconds
	}
}

1) まずい点 - new演算子

まずい点というよりは、理解不足による無駄な処理を行っていた点です。
StopWatchコンストラクタ関数の戻り値が存在します。
この時の「return { ... }」 は return new Object();」 と等価です。
ということは、関数を(new関係なく)呼び出すだけでオブジェクト(インスタンス)が生成できてしまうのです。*1
あと、これはコンストラクタ関数じゃないので、createStopWatch()みたいな感じに名称変更も行うことも必要になるでしょう。

//等価
var sw1 = new StopWatch();
var sw2 = StopWatch();

2) まずい点 - オブジェクト(インスタンス)のサイズ

各オブジェクト(インスタンス)が全メンバとメソッドを持っているので、大量にオブジェクト(インスタンス)を生成するとメモリ量が圧迫される場合があります。
プロトタイプ拡張する形式でカスタムオブジェクト定義すれば、オブジェクト(インスタンス)では各自の状態だけを持たせるようにできるため、使用メモリ量が改善が期待できます。

3) まずい点 - オブジェクト(インスタンス)がカスタムオブジェクトでは無い

コンストラクタ関数の戻り値で、new Object()して、そこにプロパティをいろいろ設定したものをインスタンスとして使用しています。
これだとObjectとして認識されていまうようです。(StopWatchオブジェクトであることを期待していました。)

var sw = new StopWatch();
console.log(sw);

★出力結果
f:id:ham007:20131010163216p

4) プロトタイプを意識したオブジェクト

定義例として以下のような記述が最も理解しやすいんじゃないかなと思います。
(ここを参考にしました)

//ストップウオッチ
var StopWatch2 = (function(){

	function StopWatch2(){
		this._st = undefined;
		this._ed = undefined;
	}

	function _start(){
		this._st = new Date();
	}
	
	function _stop(){
		this._ed = new Date();
	}
	
	var _clear = function(){
		this._st = undefined;
		this._ed = undefined;
	}
	var _getElapsedMilliseconds = function(){
		if(this._st === undefined || this._ed === undefined)
			return -1;
		return this._ed.getTime() - this._st.getTime();
	}
	
	StopWatch2.prototype = {
		constructor : StopWatch2,
		start : _start,
		stop : _stop,
		getElapsedMilliseconds : _getElapsedMilliseconds
	}
	
	return StopWatch2;
})();

5) プロトタイプを意識したオブジェクト - メリット

  • オブジェクト(インスタンス)サイズ量が小さくなる。オブジェクト共通で使うものはprototype経由で参照できる。

6) プロトタイプを意識したオブジェクト - デメリット

  • メンバの隠蔽が出来ない。JavaScriptにアクセス修飾の概念が存在しない為。やろうと思うとクロージャを使う必要あり。

7) 定義方法の使い分け

一長一短ありますので、状況に応じて使い分けするのもよいかと思いますが、
常に「プロトタイプを意識したオブジェクト」の定義方法を採用するのでも問題ないかと思います。
メンバの隠蔽の件については、コーディング規約など、運用でカバーできますしね。。

*1:コンストラクタ関数に戻り値が無い場合は、生成したthisに相当するオブジェクトが返されます。

javascriptで再帰処理

こんな感じで書きました。

<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-type" content="text/html; charset=utf-8" />
    <title>再帰呼び出しサンプル</title>
  </head>
  <body>
    <p id="msg_aisatu1">おはよう</p>
    <p id="msg_aisatu2">こんにちは</p>
    <p id="msg_aisatu3">おげんきですか</p>
    <p id="msg_aisatu4">調子はどうだい?</p>
  </body>
  <script type="text/javascript">
    var Sample = function(){
      var _getUniqId = function(id){
        //javascriptはvoid関数が無いため、このように対応した
        var result = id;
        var func = function(id, cnt){
          var searchId = id + (cnt ? cnt : 1);
          var tgtEle = document.getElementById(searchId);
          if(tgtEle !== null)
            func(id, cnt ? ++cnt : 1);
          else
            result = searchId;
        }
        func(id);
        return result;
      }
      
      return{ getUniqId : _getUniqId }
    };
  
    var obj1 = new Sample();
    var result = obj1.getUniqId("msg_aisatu");
    
    //検索結果:msg_aisatu5
    console.log("検索結果:" + result);
  </script>
</html>

属性値の取得、変更フック

jQuery1.6の新機能を試してみました。こんな感じで変更を感知できます。
参照元:jQuery1.6の更新内容をまとめたよ。 | Ginpen.com
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<script src="http://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.6.min.js"></script>
<script type="text/javascript">
  $(document).ready(function(){
    $("a").hover(
      function(){
        $(this).text($(this).attr("href"));
      },
      function(){
        $(this).text($(this).data("link_title"));
        $("#message1").text("");
      }
    );
  
    $.attrHooks.href = {
      //elem:domオブジェクト
      //value:フックした属性名
      get : function(elem, value){
        $("#message1").text("hrefが参照されました");
      },
      set : function(elem, value){
      	//設定されたときの処理
      }
    };
    
    $("#button1").click(function(){
      $("#result1").text("文字数:" + $("#text1").val().length);
    });
    
    $.valHooks.text = {
      get : function(elem, value){
        $("#message2").show().text("テキストボックスのtextが参照されました。").fadeOut("slow");
      },
      set : function(elem, value){
      	//設定されたときの処理
      }
    };
    
  });
</script>
</head>
<body>
<p><a id="link1" href="http://wwww.yahoo.co.jp/" data-link_title="ヤホー">ヤホー</a></p>
<p><a id="link2" href="http://wwww.google.co.jp/" data-link_title="グーグル">グーグル</a></p>
<p><a id="link3" href="http://wwww.livedoor.com/" data-link_title="ライブドア">ライブドア</a></p>
<span id="message1"></span>
<hr />
<p><input id="text1" type="text" value="" /><input id="button1" type="button" value="ボタン" /><span id="result1"></span></p>
<span id="message2"></span>
</body>
</html>

時間帯重複チェック

お題:時間帯重複チェック - No Programming, No Life
だらだらとなってしまいました

<html>
<head>
<script type="text/javascript">
var DateRange = function(stH, edH, stM, edM){
  var checkHour = function(obj){
    return 0 <= obj.stHour <= 24 && 0 <= obj.edHour <= 24;
  };
  
  var checkMinitue = function(obj){
    return 0 <= obj.stMinitue <= 59 && 0 <= obj.edMinitue <= 59;
  };
  
  var checkDateRange = function(obj){
    return obj.stHour < obj.edHour ? true : obj.stMinitue <= obj.edMinitue;
  };
  
  var tostring = function(val){
    return val <= 9 ? "0" + val.toString() : val.toString();
  }
  
  this.stHour = parseInt(stH, 10);
  this.edHour = parseInt(edH, 10);
  this.stMinitue = parseInt(stM, 10);
  this.edMinitue = parseInt(edM, 10);

  this.inRange = function(dateRange){
    if(this.stHour <= dateRange.stHour && dateRange.stHour <= this.edHour){
      if(Math.abs(this.edMinitue - this.stMinitue) > 1)
        return true;
    }
    return false;
  };
  
  this.toString = function(){
    return tostring(this.stHour) + ":" + tostring(this.stMinitue)  + "〜" + tostring(this.edHour) + ":" + tostring(this.edMinitue);
  };
  
  if(!checkHour(this) || !checkMinitue(this) || !checkDateRange(this)){
    throw {
      name : "InvalidValue",
      message : "時間範囲不正"
    };
  }
};

function onClick(){
  var getRange = function(rangeSrc){
    return new DateRange(rangeSrc.sth, rangeSrc.edh, rangeSrc.stm, rangeSrc.edm);
  }
  
  var datas = [
    [ {sth : 1, stm : 0, edh : 5, edm : 30}, {sth : 9, stm : 0, edh : 23, edm : 0} ]  //重複なし
    , [ {sth : 1, stm : 0, edh : 2, edm : 0}, {sth : 2, stm : 0, edh : 3, edm : 0} ]  //重複なし
    , [ {sth : 1, stm : 0, edh : 2, edm : 1}, {sth : 1, stm : 59, edh : 3, edm : 0} ]  //重複あり
    , [ {sth : 1, stm : 0, edh : 2, edm : 0}, {sth : 1, stm : 0, edh : 2, edm : 0} ]  //重複なし
    , [ {sth : 10, stm : 15, edh : 10, edm : 45}, {sth : 9, stm : 15, edh : 12, edm : 55} ]  //重複あり
  ];
  
  for(var i=0;i<datas.length;i++){
    try{
       var range1 = getRange(datas[i][0]);
       var range2 = getRange(datas[i][1]);
       
       var msg = "(" + range1.toString() + " vs " + range2.toString() + ")";
       if(range1.inRange(range2) || range2.inRange(range1))
         msg = "重複あり  " + msg;
       else
         msg = "重複なし  " + msg;
     }catch(e){
       msg = e.message;
     }
     window.alert(msg);
  }
}
</script>
</head>
<body>
<input type="button" value="チェック" onclick="onClick();">
</body>
</html>

apply関数


  • Functionオブジェクトのapply関数を呼び出すことで、thisを差し替えての関数呼び出しができる!

  • 第2パラメーターには関数パラメーターを配列で指定する。

サンプルその1
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<script type="text/javascript">
var Foo = function(msg){
	this.msg = msg;
};
Foo.prototype.getMessage = function(){
	return this.msg;
};

function pageLoad(){
	var foo = new Foo("Fooインスタンス");
	var obj = {msg : "差し替えインスタンス"};
	window.alert(foo.getMessage.apply(obj));
}
</script>
<body onload="pageLoad();">
</body>
</html>
サンプルその2

コンストラクタ関数のapply関数を呼び出すと、オブジェクトの特性を合成することができる!!
また、継承やオーバーロードのようなこともapply()で可能。
参照元:JavaScriptの再利用とapply | 勉強するのが、そんなに偉い訳!?

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<script type="text/javascript">
var Hoge = function(){
	this.getHoge = function(){
		return "ほげ";
	};
};

var Foo = function(){
	this.getFoo = function(){
		return "ふー";
	};
};

var Hello = function(name){
	this.Name = name;
	this.getAisatu = function(){
		return "はろー" + this.Name;
	};
}

function pageLoad(){
	var mixedObj = {};
	Hoge.apply(mixedObj );
	Foo.apply(mixedObj );
	Hello.apply(mixedObj , ["太郎"]);
	window.alert(mixedObj .getHoge() + "、" + mixedObj .getFoo() + "、" + mixedObj .getAisatu());
}
</script>
<body onload="pageLoad();">
</body>
</html>