初めてのVue.js（jQuery脳からの移行をメインに）

# 記事概要

jQueryベースでWebブラウザベースのアプリ書いている人がVue.jsに手を出すとしたら、「こんな風にやってみると分かりやすいかも？」と言う話。**ES5 前提**です。

Vue.jsの特徴として、他のフレームワークとの混在が容易、ってのがある。なのでjQueryとの共存も可能。今回の例では「inputタグなどの入力インターフェースにのみVue.jsを適用し、設定パネルのスライドダウン／アップなどの表示系DOMの操作はjQueryでのコードをそのまま流用」という方針で組んでみた。

「Vue.jsとは何ぞや？」は下記の公式ガイドを参照のこと。
https://jp.vuejs.org/v2/guide/
> Vue (発音は / v j u ? / 、 view と同様）はユーザーインターフェイスを構築するためのプログレッシブフレームワークです。他の一枚板(モノリシック: monolithic)なフレームワークとは異なり、Vue は初めから少しづつ適用していけるように設計されています。

なお、当方は**MVVMに対する理解が全力で不足**の状態で書いているので、そこは留意のこと。


# Vue.jsの考え方

Vueを適用したブロックタグ配下において、inputタグの内容をjavascript側に閉じて（タグ側のDOMとしての操作を意識することなく）操作できる。たとえば、inputタグを変数 var param にバインドしておけば、javascript側でparamの値に対してI/Oするだけでinputタグ側に勝手にVueが反映してくれる（※双方向か、一方向かも指定可能）。

なお、html側のタグ構造もVue側の`component` 機能を利用することでVue側で管理できる様子だけど、今回は割愛。タグの構造はhtmlで直に書き下すこととする。

```
// 動作イメージ：

<input value="ほげ">
var input_str = "ふが"; // この変数へのI/Oだけでinputタグは勝手に追随する。
```
 


# jQueryとの共存について

Vueを適用したいブロック（例：`<div id="id_app"><div>`）に対して `new Vue({el: '#id_app'});`でバインドする。バインドしたノード配下がVueの動作範囲になる。この外側でJQueryを使うことで共存が可能。なお、Vueの動作範囲ではイベント発火はVueがハンドリングするので、jQuery側からイベントのバインドはしないこと。

以下のような構成が可能。

＜BODY＞
＜Vueが動作するブロック＞＜／＞
＜jQueryが動作するブロック＞＜／＞
＜jQueryが動作するブロック＞
　＜Vueが動作するブロック＞＜／＞
＜／＞
＜/BODY＞


# サンプルコード

以下の機能を持つWebページを作成してみる。

1. inputタグに入力した文字列を「保存」ボタンでCookieに保存
2. inputタグに入力した文字列を「追加」ボタンでプルダウンメニュー（selectタグ）に追加
3. プルダウンメニューから項目を選択すると、inputタグに反映される。
4. 次回のページ読み込み時に、「保存」した文字とプルダウンメニューは、自動反映される。

※コード中に「Azure」の文字があるのは、引用元がAzure関連のソースのため。このサンプルでは**特に意味はないので無視**すること(後でトリミングします)。

## デモ画面

サンプルコードのデモは[こちら](http://fluorite.halfmoon.jp/diary/2017/vue_sample/index_qiita.html)。

## スクリーンショット

動作の様子はこちら。
設定パネルの開閉をjQueryで、値へのjQueryからのRead/WriteをVue.jsで実装。

![スクリーンショット 2017-04-18 12.54.06.png](https://qiita-image-store.s3.amazonaws.com/0/125588/21fbeff7-e96c-e037-f22b-b2ce2cdba1c0.png)

![スクリーンショット 2017-04-18 12.54.01.png](https://qiita-image-store.s3.amazonaws.com/0/125588/c2e81955-a644-55da-b4aa-3615eede634d.png)


## 前準備 - 利用モジュールのDL

以下のモジュールを利用する。

- Vue.js
- jQuery2.x
- tiny-cookie

これらのうち、Vue.jsとjQueryはCDNを利用するのでダウンロード不要。tiny-cookieについては、Node.jsのnpmコマンドが使える人は`npm install tiny-cookie`から、そうでない人は https://www.npmjs.com/package/tiny-cookie からGitHubを辿って取得する。ダウンロードしたtiny-cookieモジュールを、本サンプルでは`./lib/tiny-cookie/tiny-cookie.js`に配置する。


## htmlファイル＋CSSファイル

ユーザーへの見せ方を構成する。inputやselectタグの配置もここで記述する。それぞれのタグに、JavaScript側のどの変数をバインドして、どのイベント（クリック）に反応させるか？もここで記述する（※JavaScript側に取り込む方法もあるっぽいけど今回は割愛）。合わせて、**未入力状態の警告表示**や入力後にボタン表示、などもここでhtmlタグで設定できる。

Vue.jsに基づいたバインド関連で、やっていることは以下。

- 未入力時の警告表示用タグに、` v-if`属性をつけて表示トリガーを指定。
- 未入力時の状態で非表示にしたいタグ（ボタン）に、`v-show`属性を指定。
- 値をバインドしたいタグに、`v-model`属性を用いてバインドする変数名を指定。
- クリック動作させたいタグに、`v-on:click`属性を利用して発火させる関数を指定。


なお、簡単のためCSSはhtmlファイル内に記述した。

```index_qiita.html
<html>
<head>
	<meta charset="UTF-8" />
	<meta name="viewport" id="id_viewport" content="width=device-width" >

	<style>
		.error {
			font-size: 10px;
			color: red;
			font-weight: bold;
		}
		input {
			width: 240pt;
		}
		select {
			width: 240pt;
		}
	</style>


	<!-- vue.jsの読み込み -->
	<script src="https://unpkg.com/vue/dist/vue.js"></script>

	<!-- cookie容易に扱うヤツの読み込み -->
	<script src="./lib/tiny-cookie/tiny-cookie.js"></script>

	<!-- vue.jsベースで描画 -->
	<script src="./vue_main_qiita.js"></script>


	<!-- jQueryとの共存OK～♪ -->
	<script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>

	<!-- jQueryベースで描画 -->
	<script src="./ui_panel_qiita.js"></script>
	<script>
		$(document).ready(function(){
			var panelbutton = bindExpandCollapsePanel( "id_ctrl_panel" )
		});
	</script>
</head>
<body>
	<div id="id_ctrl_panel">
		<!-- ※以下のdiv配下のイベントはvue.jsに捉まれるので、jQueryから操作NG -->
		<div id="app">
			<div>
				ドメイン：
				<span class="error" v-if="!azure_domain_str">ドメインを入力して「保存」を押してください</span><br>
				<input v-model="azure_domain_str">
				<button v-on:click="add_azure" v-show="azure_domain_str">保存</button><br>
			</div>
			<br>
			<div>
				デバイスキー＋表示名：
				<span class="error" v-if="!device_key_str">デバイスキーを入力してください。</span>
				<span class="error" v-if="!device_name_str">表示名を入力してください。</span>
				<br>
				<input v-model="device_key_str"><br>
				<input v-model="device_name_str">
				<button v-on:click="add_device" v-show="device_key_str && device_name_str">追加</button><br>
			</div>
			
			<div style="display:none;"><!-- // debugger -->
				<div id="id_azure_domain">{{ azure_domain_str }}</div>
				<div id="id_device_key">{{ device_key_str }}</div>
			</div>
		</div>
	</div>
	<hr>
	<!-- jQueryとの共存のために、↑↓別々のdivとして２つのvueインスタンスとして扱う。 -->
	<div>
		<div id="app_selector">
			<select v-model="selected" v-on:change="update_inputer">
				<option v-for="item in options" v-bind:value="item.value">
					{{ item.text }}
				</option>
			</select>
			<button v-on:click="update_chart">実行（Alert）</button><br>
			<!-- // debugger -->
			<div style="display:none;"><!-- // debugger -->
				対応するValue⇒ {{ selected }}
			</div>
		</div>
	</div>
	<br>
	<br>
	<br>
</body>
</html>
```

## JavaScriptファイル - Vue.js

Vue.jsを利用して定義する。new Vue({});で、引数にhtmlファイル側のidなどを利用して紐づけを指定する。生成したVueのインスタンスに対してデータの入出力（この例では、inputタグの内容をSelectタグに追加する、等）をすれば、勝手にhtml側にも反映される。html側を意識しなくてよい。便利♪

なお、Vueのインスタンス生成は「htmlのDOMが描画されてから」実施する必要がある。多くのVueの例だと、htmlタグを記述し終えた後に `<script>`で入れてるのは、そういう背景。今回の例では、`window.onload`を利用して「描画後」にVueインスタンスを生成している。

※ソース中のコメントで「後でマージ」は、このサンプルでJQuery側でイベント処理している部分（ペインの開閉）もVue側に取り込めば、Vueのインスタンスを分ける必要がなくなり「マージできる」という意図。


```vue_main_qiita.js
/*
    [vue_main_qiita.js]
    encoding=UTF-8
*/


window.onload = function(){
	var items = loadItems();
	var app = new Vue({
		el: '#app',
		data: function(){
			return {
			"azure_domain_str" : loadAzureDomain(),
			"device_key_str"   : "",
			"device_name_str"  : ""
			};
		},
		methods : {
			"add_azure" : function(event){
				saveAzureDomain( this.azure_domain_str );
			},
			"add_device" : function(e){
				add_selecter_if_unique( this, app2 ); // 後でマージする。⇒this１つになる。
				saveItems( app2.options ); // 後でマージする。⇒this.optionsになる。
			}
		}
	});
	var app2 = new Vue({ // jQueryとの共存の都合で分ける。
		el: '#app_selector',
		data: function(){
			return {
			// 以下はセレクター関連
			"selected" : "", // ここは初期選択したいvalueを指定する。
			"options" : items
			};
		},
		methods : {
			"update_inputer" : function(e){
				show_item_on_inputer( this, app ); // 後でマージする。⇒this１つになる。
			},
			"update_chart" : function(e){
				update_log_viewer( app ); // 後でマージする。⇒thisになる。
			}
		}
	});
	var add_selecter_if_unique = function( src, dest ){
		var list = dest.options, n = list.length, is_unique = true;
		while( 0<n-- ){
			if( list[n].value == src.device_key_str ){
				is_unique = false;
				break;
			}
		}
		if( is_unique ){
			dest.options.push({
				value : src.device_key_str,
				text  : src.device_name_str
			});
		}
	};
	var show_item_on_inputer = function( src, dest ){
		var selected_value = src.selected;
		var list = src.options, n = list.length;
		while( 0<n-- ){
			if( list[n].value == selected_value ){
				dest.device_key_str = list[n].value;
				dest.device_name_str = list[n].text;
				break;
			}
		}
	};
	var update_log_viewer = function( src ){
		var str = src.azure_domain_str + "\r\n";
		str += src.device_key_str;
		alert( "この先でjQueryベースの関数を呼び出すこともOK。\r\n\r\n" + str )
	};
};



/**
 * Cookieを利用したデータ保存。
 *
 */
var MAX_LISTS = 7;
var COOKIE_NAME  = "AzBatteryLog_Text";
var COOKIE_VALUE = "AzBatteryLog_Value";
var COOKIE_OPTIONS = {expires: 7};
var loadItems = function(){
	var cookie = window.Cookie;
	var list = [];
	var name, value, n = MAX_LISTS;
	while( 0 < n-- ){
		name = cookie( COOKIE_NAME + n );
		value = cookie( COOKIE_VALUE + n );
		if( name && value ){
			list.push({
				"text" : name,
				"value" : value
			});
		}
	}
	return list;
};
var saveItems = function( list ){
	var cookie = window.Cookie;
	var name, value, n = MAX_LISTS;
	while( 0 < n-- ){
		if( list[n] && list[n].text && list[n].value ){
			name = cookie( COOKIE_NAME + n, list[n].text, COOKIE_OPTIONS );
			value = cookie( COOKIE_VALUE + n, list[n].value, COOKIE_OPTIONS );
		}
	}
};
var loadAzureDomain = function(){
	var cookie = window.Cookie;
	return cookie("AzBatteryLog_Domain");
}
var saveAzureDomain = function( azureStr ){
	var cookie = window.Cookie;
	cookie("AzBatteryLog_Domain", azureStr, COOKIE_OPTIONS );
};
```


## Javascriptファイル - jQuery

こちらでは、DOM操作によるビジュアルの変化をjQueryで実装する。
上部のバーをクリックするたびに、設定用ペインをスライドダウン／スライドアップさせる。

※おそらくは、Vue.jsだけでも（もしくは関連フレームワークで）このような視覚効果も実装可能と思われる。しかし、こういう視覚効果はJQueryの得意とするところなので、そのまま残すことで移行を容易とするのが今回の方針。

```ui_panel_qiita.js
/*
    [ui_panel_qiita.js]
    encoding=UTF-8
*/


var bindExpandCollapsePanel = function( idPanel ){
    var BGCOLOR = "#77ffcc";
    var CSS_EXPAND = {
	    "float" : "left",
        "line-height" : "0",
        "width" :  "0",
        "height" : "0",
        "border" : "10px solid " + BGCOLOR, /* transparent */
        "border-top" : "10px solid #000000",
        "padding-bottom": "0px"
    };
    var CSS_COLLAPSE = {
	    "float" : "left",
        "line-height" : "0",
        "width" :  "0",
        "height" : "0",
        "border" : "10px solid " + BGCOLOR, /* transparent */
        "border-left" : "10px solid #000000",
        "padding-bottom": "0px"
    };    
    var str = "<div id=\"\_id_expand_collapse_base\">"
    str += "<div id=\"_id_marker\"></div>";
    str += "<div id=\"_id_expand_collapse\">設定パネル</div>"
    str += "<div style=\"float: none; both: clear;\"></div>"
    str += "</div>";
    $("#"+idPanel).before(str);
    $("#"+idPanel).hide();

    $("#_id_marker").css(CSS_COLLAPSE);
    $("#_id_expand_collapse_base").css({
        "margin" : "4   px",
        "padding" : "8px",
    	"cursor" : "pointer",
        "backgroundColor" : BGCOLOR
    });
    $("#_id_expand_collapse").click(function(){
        if($("#"+idPanel).is(":visible")){
            $("#"+idPanel).slideUp();
            $("#_id_marker").css(CSS_COLLAPSE);
        }else{
            $("#"+idPanel).slideDown();
            $("#_id_marker").css(CSS_EXPAND);
        }
    });
    return $("#_id_expand_collapse");
};
```

以上ー。


# 補足（MVVMとは？など自己メモ）

MVVM（Model-View-ViewModel ）とは何ぞや？MVCとMVPの違いは？、については、以下のWebサイトが分かりやすかった。

雑把の UI アーキテクチャー史(MVCからMVVMへ)|プログラマーズ雑記帳
http://yohshiy.blog.fc2.com/blog-entry-215.html

ところで、上記のサンプルコードで`var app = new Vue({});` してるところは、`var app = factory.create({});` した方が良いのではないか？と思えてきた。せっかくU/Iからモデルを切り離しているのだから、モデルに対するテストのし易さを考慮しないともったいない。factoryのインスタンスを切り替えてあげれば、（、、ってVueというオブジェクト自体を置き換える手もあるか）好きなようにテストできるわけで。



# 参照にしたサイト様

jQueryから移行するならVue.jsが良さそう
http://tech.innovation.co.jp/2017/01/13/vue.html

Vue.js（ここが公式かな）
https://jp.vuejs.org/

フォーム入力バインディング - Vue.js（上記サイトにあるガイドの一部）
https://jp.vuejs.org/v2/guide/forms.html#%E9%81%B8%E6%8A%9E

体で覚えるVue.js - インスタンスメンバ編 〜 JSおくのほそ道 #024
http://qiita.com/hosomichi/items/ebbfcc3565bcd27f344c

tiny-cookie
https://www.npmjs.com/package/tiny-cookie