【Vue.js 2.0】コンポーネントの作り方（Materialize.css利用）

いまさらですが、Vue.jsのコンポーネントの作り方調べたので記事書きます。
今回は、Vue.js 2.0・Materializeを使って実装します。

* Vue
    https://vuejs.org/

* Materialize
    http://materializecss.com/

# コンポーネントを作る
コンポーネントの基本的な作り方はこちらを参考にしてください。（英語。日本語のページは2.0に追い付いていない様子）
https://vuejs.org/v2/guide/components.html

ざっくり、下記でコンポーネントが作れます。

* template定義
* props定義
* コンポーネント登録 （``` Vue.component('my-component', ```）


## Button
手始めに下記のButtonをコンポーネントとして作ります。
http://materializecss.com/buttons.html

```js:button.js
	var Button = Vue.extend({
		props: {
			id: String,
			href: String,
			icon: String,
			flat: Boolean,
			floating: Boolean,
			'class': String,
			disabled: Boolean,
			tabindex: {
				type: Number,
				default: 0,
			},
		},
		template: '<a ' +
					':id="id" ' +
					':class="classNames" ' +
					':href="href" ' +
					'v-on:click="onClick" ' +
					'v-on:keyup.enter="onClick"' +
					':tabindex="disabled ? -1 : tabindex"' +
					':disabled=" disabled ? \'disabled\' : null "' +
					' >' +
					'<i v-if="icon" class="material-icons left">{{icon}}</i>' +
					'<slot><!-- ラベル--></slot>' +
				'</a>',
		methods: {
			onClick: function(e) {
				if (this.$el.hasAttribute('disabled')) {
					return;
				}
				this.$emit('click', e);
			}
		},
		computed: {
			classNames: function() {
				var classes = {
					'waves-effect': true,
					'waves-light': !this.flat,
					'btn': !this.flat && !this.floating,
					'btn-flat': this.flat,
					'btn-floating': this.floating,
				};
				if (this.class) {
					classes[this.class] = true;
				}
				
				return classes;
			},
		},
	});
	Vue.component('m-button', Button);
```

### 説明
materializecssのbuttonをテンプレートにして、ちょっとしたOption（icon・flat・floating）を属性で指定できるようにしました。
あとは、disabledでclickが伝播されないようにしたり、enterでclick動作をさせたりといった感じです。

* icon
    下記の実装で制御しています。

    ```js
    '<i v-if="icon" class="material-icons left">{{icon}}</i>' +
    ```

    `v-if` でicon指定があれば `i` タグが現れるようにしています。
    これでicon属性を指定すればmaterial-iconsがleftで表示されるようになります。

* flat・floating
    下記の実装で制御しています。

    ```js
			':class="classNames" ' +

	//・・・

	classNames: function() {
		var classes = {
			'waves-effect': true,
			'waves-light': !this.flat,
			'btn': !this.flat && !this.floating,
			'btn-flat': this.flat,
			'btn-floating': this.floating,
		};
		//・・・・
		
		return classes;
	},
    ```

    `flat`・`floating` の指定によってどのclassを指定するかを制御しています。  

* click伝播とdisabled

    ```js
	'v-on:click="onClick" ' +
    ```
    `a`タグをclickしたら`onClick`をcallさせます。

    ```js
	methods: {
		onClick: function(e) {
			if (this.$el.hasAttribute('disabled')) {
				return;
			}
			this.$emit('click', e);
		}
	},
    ```
    `onClick`内で`disabled`があれば終了。
    `disabled`が無ければ`this.$emit('click', e);`でclickを伝播させます。
    これでコンポーネントを使うとき属性に`v-on:click`をつけることでイベントをフックできるようになります。

### 利用

```html:利用側.html
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Cache-Control" content="no-cache" />
<meta http-equiv="expires" content="0" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta http-equiv="content-script-type" content="text/javascript" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no" />

<title>Vueでコンポーネント作るよ</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
<link href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/css/materialize.min.css" rel="stylesheet" />

<script type="text/javascript" src="https://code.jquery.com/jquery-2.2.4.js" ></script>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.8/js/materialize.js"></script>
<script type="text/javascript" src="https://npmcdn.com/vue/dist/vue.js"></script>

<script type="text/javascript" src="button.js"></script>

</head>
<body>
	<div id="main">
		<div class="row">
			<div class="col s12 center"><h4>Buttons</h4></div>
			<div class="col s12 m6 l3 center">
				<m-button v-on:click="onButtonClick" >CAPTION</m-button>
			</div>
			<div class="col s12 m6 l3 center">
				<m-button icon="thumb_up" v-on:click="onButtonClick" >ICON</m-button>
			</div>
			<div class="col s12 m6 l3 center">
				<m-button flat v-on:click="onButtonClick" >flat</m-button>
			</div>
			<div class="col s12 m6 l3 center">
				floating
				<m-button floating icon="thumb_up" v-on:click="onButtonClick" >floating</m-button>
			</div>
			<div class="col s12 m6 l3 center">
				<m-button disabled v-on:click="onButtonClick" >disabled</m-button>
			</div>
			<div class="col s12 m6 l3 center">
				<m-button class="blue" v-on:click="onButtonClick" >class</m-button>
			</div>
		</div>
	</div>
	<script type="text/javascript" >
	/**
	 * アプリケーション本体
	 */
	new Vue({
		el: '#main',
		methods: {
			onButtonClick: function() {
				alert('click!!');
			},
		},
	});
	</script>
</body>
</html>
```

できあがるのは下図のような形
clickすると`alert('click!!');`が呼ばれる。

![image](https://qiita-image-store.s3.amazonaws.com/0/84259/786a0686-3714-ff37-e779-f6dc38bf97cd.png)



## Dropdown
次はDropdownをコンポーネントとして作ります。
http://materializecss.com/dropdown.html

```js
	//http://qiita.com/coa00@github/items/679b0b5c7c468698d53f
	function getUniqueStr(myStrong) {
		var strong = 1000;
		if (myStrong) { strong = myStrong; }
		return new Date().getTime().toString(16) + Math.floor(strong * Math.random()).toString(16);
	}

	var DropdownButton = Button.extend({
		props: {
			dropdownId: {
				type: String,
				default: function() {
					return getUniqueStr() + '-dropitems';
				},
			},
		},
		template: '<div>' +
					'<a ' +
						':id="id" ' +
						'ref="button" ' +
						':class="classNames" ' +
						':tabindex="disabled ? -1 : tabindex"' +
						':disabled=" disabled ? \'disabled\' : null "' +
						':data-activates="dropdownId" ' +
						' >' +
						'<i v-if="icon" class="material-icons left">{{icon}}</i>' +
						'<slot name="label" ><!-- ラベル--></slot>' +
					'</a>' +
					'<ul ref="dropdown" :id="dropdownId" class="dropdown-content" >' +
						'<slot ><!-- Dropdowns--></slot>' +
					'</ul>' +
				'</div>',
		mounted: function() {
			$(this.$refs.button).dropdown();
		},
		methods: {
			open: function() {
				$(this.$refs.button).dropdown('open');
			},
			close: function() {
				$(this.$refs.button).dropdown('close');
			},
		},
	});
	var DropItem = Vue.extend({
		props: {
			id: String,
			divider: Boolean,
			href: String,
		},
		template: '<li :id="id" :class="classNames">' +
					'<a ' +
						'v-if="!divider" ' +
						'ref="anchor" ' +
						':href="href" ' +
						'v-on:click="onClick"' +
						'>' +
						'<slot ><!-- ラベル--></slot>' +
					'</a>' +
				'</li>',
		methods: {
			onClick: function(e) {
				this.$emit('click', e);
			}
		},
		computed: {
			classNames: function() {
				return {
					divider: this.divider,
				};
			},
		},
	});
	Vue.component('m-dropdown-button', DropdownButton);
	Vue.component('m-dropitem', DropItem);
```

### 説明
今回はdropdow-buttonとdropitemで分けました。  
あと、DropdownButtonは先に作成したButtonを継承しています。  

```js
var DropdownButton = Button.extend({
```

* dropdownId
    buttonの`a`タグとdropdownの`ul`タグを紐づけるためのidです。
    指定がないときのために乱数をはめてます。
    乱数ロジックは[こちら](http://qiita.com/coa00@github/items/679b0b5c7c468698d53f)を参考にしました。

    ```js
	//http://qiita.com/coa00@github/items/679b0b5c7c468698d53f
	function getUniqueStr(myStrong) {
		var strong = 1000;
		if (myStrong) { strong = myStrong; }
		return new Date().getTime().toString(16) + Math.floor(strong * Math.random()).toString(16);
	}

    //・・・・
		props: {
			dropdownId: {
				type: String,
				default: function() {
					return getUniqueStr() + '-dropitems';
				},
			},
    ```

* mountedでdropdown化
    materializecssでdropdown化するには`$(hoge).dropdown();`を呼ばないといけません。
    Vue.js2のmountedのときにこれを呼び出します。  

    ```js
		mounted: function() {
			$(this.$refs.button).dropdown();
		},
    ```


### 利用

```html:利用側.html
		<div class="row">
			<div class="col s12 center"><h4>Dropdown Buttons</h4></div>
			<div class="col s12 m6 l3 center">
				<m-dropdown-button>
					<span slot="label">CAPTION</span>
					<m-dropitem v-on:click="onButtonClick" >Item1</m-dropitem>
					<m-dropitem divider ></m-dropitem>
					<m-dropitem v-on:click="onButtonClick" >Item2</m-dropitem>
				</m-dropdown-button>
			</div>
			<div class="col s12 m6 l3 center">
				<m-dropdown-button icon="thumb_up" >
					<span slot="label">ICON</span>
					<m-dropitem v-on:click="onButtonClick" >Item1</m-dropitem>
					<m-dropitem divider ></m-dropitem>
					<m-dropitem v-on:click="onButtonClick" >Item2</m-dropitem>
				</m-dropdown-button>
			</div>
			<div class="col s12 m6 l3 center">
				floating
				<m-dropdown-button floating icon="thumb_up" >
					<m-dropitem v-on:click="onButtonClick" >Item1</m-dropitem>
					<m-dropitem divider ></m-dropitem>
					<m-dropitem v-on:click="onButtonClick" >Item2</m-dropitem>
				</m-dropdown-button>
			</div>
		</div>
```

![image](https://qiita-image-store.s3.amazonaws.com/0/84259/b12538d9-d5fa-016b-8b92-4e1f2f0cbf26.png)
![image](https://qiita-image-store.s3.amazonaws.com/0/84259/728ff155-746f-a2c9-2074-16cdc9223235.png)


## Input
次はInputです。
http://materializecss.com/forms.html

```js
	//Materialize.updateTextFields()を最後の1回だけ実行する
	var updateTextFieldsTimeoutId;
	var updateTextFields = function() {
		if (updateTextFieldsTimeoutId) {
			clearTimeout(updateTextFieldsTimeoutId);
		}
		updateTextFieldsTimeoutId = setTimeout(function() {
			Materialize.updateTextFields();
		}, 100);
	};
	var isNumber = function(s) {
		if ('' === s) {
			return true;
		}
		return !isNaN(s - 0);
	};

	var numseparate = function(num) {
		if (isNumber(num)) {
			num = num + '';
			var spl = num.split('.');
			spl[0] = spl[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');

			return 1 === spl.length ? spl[0] : spl[0] + '.' + spl[1];
		} else {
			return num;
		}
	};
	var numunseparate = function(numstr) {
		if ('' === numstr) {
			return '';
		}
		var num = (numstr + '').trim().replace(/,/g, '') - 0;
		return isNaN(num) ? numstr : num;
	};

	var Input = Vue.extend({
		props: {
			id: String,
			value: {
				type: [String, Number],
				default: ''
			},
			label: String,
			icon: String,
			placeholder: String,
			required: Boolean,
			readonly: Boolean,
			type: {
				type: String,
				default: 'text',
			},
		},
		// https://vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events
		template: '<div class="input-field" >' +
					'<i v-if="icon" class="material-icons prefix">{{icon}}</i>' +
					'<input ' +
						':id="id" ' +
						'ref="input" ' +
						':type="type === \'number\' ? \'text\' : type" ' +
						':placeholder="placeholder" ' +
						':required="required ? \'true\' : null" ' +
						':aria-required="required ? \'true\' : null" ' +
						':readonly="readonly ? \'true\' : null" ' +
						':class="inputClassNames" ' +
						'v-on:input="updateValue($event.target.value)" ' +
						'v-on:blur="onBlur" ' +
						'v-on:focus="onFocus" ' +
						'/>' +
					'<label ref="label" :for="id" >{{label}}</label>' +
				'</div>',

		watch: {
			value: function(val, oldVal) {
				if (document.activeElement === this.$refs.input) {
					return;
				}
				this.$refs.input.value = this.valueToDisplay(val);
			},
		},
		methods: {
			updateValue: function(value) {
				this.$emit('input', this.inputToValue(value));
			},
			onBlur: function(e) {
				this.blurChangeInputValue(this.$refs.input);
				this.$emit('blur', e);
			},
			onFocus: function(e) {
				this.focusChangeInputValue(this.$refs.input);
				this.$emit('focus', e);
			},
			validate: function() {
				var value = this.inputToValue(this.$refs.input.value);

				if (this.required && (null === value || 'undefined' === typeof value || '' === value)) {
					this.setInvalid('empty');
					return false;
				}
				if ('number' === this.type && !isNumber(value)) {
					this.setInvalid('not a number');
					return false;
				}
				this.$refs.input.classList.remove('invalid');
				this.$emit('validate', value, this);
				return this.$refs.input.classList.contains('invalid');
			},
			setInvalid: function(msg) {
				if (msg) {
					this.$refs.label.setAttribute('data-error', msg);
					this.$refs.label.classList.add('active');
					this.$refs.input.classList.add('invalid');
				}
			},
			valueToDisplay: function(value) {
				if ('number' === this.type) {
					return numseparate(value);
				}
				return value;
			},
			inputToValue: function(value) {
				if ('number' === this.type) {
					return numunseparate(value);
				}
				return value;
			},
			blurChangeInputValue: function(input) {
				if ('number' === this.type) {
					var num = input.value.trim() - 0;
					if (!isNaN(num)) {
						input.value = numseparate(num);
					}
				}
			},
			focusChangeInputValue: function(input) {
				if ('number' === this.type) {
					input.value = this.inputToValue(input.value);
				}
			},
		},
		mounted: function() {
			this.$refs.input.value = this.valueToDisplay(this.value);
			updateTextFields();
			//materializeのvalidationに無理やり乗せる
			this.$refs.input['$ validate $'] = this.validate.bind(this);

			//なぜか再レンダリングされるので抑制（良い方法がわからない。1度再レンダリングさせると起きなくなったりするので、1回だけ再レンダリングさせる）
			setTimeout(function() {
				var val = this.value;
				this.$emit('input', val + '-');//適当な値に変更
				this.$emit('input', val);
			}.bind(this), 100);
		},
		computed: {
			inputClassNames: function() {
				return {
					'validate': true,
					'right-align': 'number' === this.type,
				};
			},
		},
	});
	Vue.component('m-input', Input);

	$(document).ready(function() {
		//materializeのvalidationを無理やり拡張する
		var origValidateField = window.validate_field;
		window.validate_field = function($obj) { //eslint-disable-line camelcase
			origValidateField($obj);

			if ($obj[0]['$ validate $']) {
				$obj[0]['$ validate $']();
			}
		};
	});
```

### 説明
（なんだこれ？的な記述の説明は後回しです。）


* v-modelを利用できるようにする
    こちらに記載があります。
    https://vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events
    `value`のプロパティを用意すると`v-model`で指定された実際の値が取り出せ、
    値の変更時に`this.$emit('input', 新規値)`をcallすることでmodelに値を返せるようです。

    ただ今回`type=number`の場合にカンマ区切りにするということがやりたかったので、少し大変な目にあいました。

    難しいことしないのであれば下記のような記述で問題ないと思います。

    ```
       v-bind:value="value"\
       v-on:input="$emit(\'input\', $event.target.value)"\
    ```

    一応、処理上の注意ですが、`v-model`が外部から指定されない時、`$emit('input'・・`しても、プロパティ`value`の値は変わらないようなので、`mounted`と`watch.value`以外は常にinput.valueから値を取得して処理するのが良さげです。

    ```js
	//var value = this.value; これだと値が変わってないことがあるのでまずい。
	var value = this.$refs.input.value;
    ```

* numberとカンマ区切り
    （全然materializecss関係ないです）
    このコンポーネントは`type="number"`の場合、数値をカンマ区切りするコンポーネントに変わります。
    カンマ区切りにするというのもあり、`':type="type === \'number\' ? \'text\' : type" ' +`の記述で、
    内部では`type="text"`に戻しています。  

    カンマ区切りにするためにinputに直接値をbindできないのでvalueToDisplayメソッドを通します。
    また、inputにフォーカス中はカンマ区切りを解除するというのもあり、
    `v-bind:value="valueToDisplay(value)"`にしてしまうと、入力中にmodelから再bindされるとき、入力中なのにカンマ区切りに戻るという事象が発生するので、
    input値のセットは、`mounted`と`watch.value`で行うことにしました。`watch.value`はフォーカス中であればinputの値を書き換えないという制御を入れています。  

    ```js
	var numseparate = function(num) {
		if (isNumber(num)) {
			num = num + '';
			var spl = num.split('.');
			spl[0] = spl[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, '$1,');

			return 1 === spl.length ? spl[0] : spl[0] + '.' + spl[1];
		} else {
			return num;
		}
	};
    //・・・・
		watch: {
			value: function(val, oldVal) {
				if (document.activeElement === this.$refs.input) {
					return;
				}
				this.$refs.input.value = this.valueToDisplay(val);
			},
		},
    //・・・・
			valueToDisplay: function(value) {
				if ('number' === this.type) {
					return numseparate(value);
				}
				return value;
			},
    //・・・・
		mounted: function() {
			this.$refs.input.value = this.valueToDisplay(this.value);
    ```

    あとはフォーカスしたらカンマ区切りが外れたり、フォーカス抜けたらカンマ区切りに戻したりしてます。

* Materialize.updateTextFields()
    `Materialize.updateTextFields()`はdocument全体にかかる方法しかないようで、何度も呼びだしたくないので、
    遅延させて最後の1回だけcallさせるようにしました。  
    アプリ側から任意のタイミングで呼び出せるならこの記述はいらないと思いますが、コンポーネントとしては、「アプリで書く」というルールは微妙なので、mountedから呼びだすようにしています。  

    ```js
	var updateTextFieldsTimeoutId;
	var updateTextFields = function() {
		if (updateTextFieldsTimeoutId) {
			clearTimeout(updateTextFieldsTimeoutId);
		}
		updateTextFieldsTimeoutId = setTimeout(function() {
			Materialize.updateTextFields();
		}, 100);
	};
	//・・・
		mounted: function() {
			this.$refs.input.value = this.valueToDisplay(this.value);
			updateTextFields();
    ```

* validation
    Materializeのvalidationに乗せて独自のvalidationをかけたかったので黒魔術で対応しました。

    ```js
		mounted: function() {
			//・・・
			//materializeのvalidationに無理やり乗せる
			this.$refs.input['$ validate $'] = this.validate.bind(this);

	//・・・
	$(document).ready(function() {
		//materializeのvalidationを無理やり拡張する
		var origValidateField = window.validate_field;
		window.validate_field = function($obj) { //eslint-disable-line camelcase
			origValidateField($obj);

			if ($obj[0]['$ validate $']) {
				$obj[0]['$ validate $']();
			}
		};
	});
    ```

    input要素にvalidateメソッドを乗せて、materializeから呼ばれる、window.validate_fieldを上書きし、
    input要素に乗せたvalidateメソッドを実行するようにしました。  
    これで、必須チェックや数値チェック、また`v-on:validate`でvalidateを拡張することができるようになりました。  


* 悩み
   下記部分の上手い回避方法がわからず悩んでいます。  

    ```js
			//なぜか再レンダリングされるので抑制（良い方法がわからない。1度再レンダリングさせると起きなくなったりするので、1回だけ再レンダリングさせる）
			setTimeout(function() {
				var val = this.value;
				this.$emit('input', val + '-');//適当な値に変更
				this.$emit('input', val);
			}.bind(this), 100);
    ```


### 利用

```html:利用側.html
		<div class="row">
			<div class="col s12 center"><h4>Inputs</h4></div>
			<div class="col s12 m6 l3" style="height: 120px;">
				<m-input label="input" v-model="inputValue1" ></m-input>
				{{inputValue1}}
			</div>
			<div class="col s12 m6 l3" style="height: 120px;">
				<m-input placeholder="placeholder" label="placeholder" v-model="inputValue2" ></m-input>
				{{inputValue2}}
			</div>
			<div class="col s12 m6 l3" style="height: 120px;">
				<m-input type="number" label="number" v-model="inputValue3" ></m-input>
				{{inputValue3}}
			</div>
			<div class="col s12 m6 l3" style="height: 120px;">
				<m-input required label="required" v-model="inputValue4" ></m-input>
				{{inputValue4}}
			</div>
			<div class="col s12 m6 l3" style="height: 120px;">
				<m-input readonly label="readonly" v-model="inputValue5" ></m-input>
			</div>
			<div class="col s12 m6 l3" style="height: 120px;">
				<m-input icon="account_circle" label="icon" v-model="inputValue6" ></m-input>
				{{inputValue6}}
			</div>
			<div class="col s12 m6 l3" style="height: 120px;">
				<m-input label="validate" v-model="inputValue7" v-on:validate="validate" ></m-input>
				{{inputValue7}}
			</div>
		</div>
```

```js
	/**
	 * アプリケーション本体
	 */
	new Vue({
		el: '#main',
		data: {
			inputValue1: '',
			inputValue2: '',
			inputValue3: '1234',
			inputValue4: '',
			inputValue5: '読み取り専用',
			inputValue6: '',
			inputValue7: '',
		},
		methods: {
			validate: function(val, input) {
				input.setInvalid('エラーメッセージ');
			},
		},
	});
```

![image](https://qiita-image-store.s3.amazonaws.com/0/84259/aca059b5-9c52-b8d0-b736-e7e668d19e84.png)
![image](https://qiita-image-store.s3.amazonaws.com/0/84259/4b811cfc-d33f-c9de-50b0-ee70176d38c4.png)

## Datepicker

```js
	var timezoneOffsetMs = new Date().getTimezoneOffset() * 60000;
	var localToUTC = function(date) {
		if (!date) {
			return date;
		}
		var utc = date.getTime() - timezoneOffsetMs;
		return new Date(utc);
	};
	var utcToLocal = function(utc) {
		if (!utc) {
			return utc;
		}
		var local = utc.getTime() + timezoneOffsetMs;
		return new Date(local);
	};
	var utcFormat = function(date, format) {
		if (!date) {
			return '';
		}
		if (isNaN(date)) {
			return '';
		}
		if (!format) {
			return date.toISOString();
		} else {
			//参考：http://qiita.com/osakanafish/items/c64fe8a34e7221e811d0
			format = format.replace(/yyyy/g, date.getUTCFullYear());
			format = format.replace(/MM/g, ('0' + (date.getUTCMonth() + 1)).slice(-2));
			format = format.replace(/dd/g, ('0' + date.getUTCDate()).slice(-2));
			format = format.replace(/hh/g, ('0' + date.getUTCHours()).slice(-2));
			format = format.replace(/mm/g, ('0' + date.getUTCMinutes()).slice(-2));
			format = format.replace(/ss/g, ('0' + date.getUTCSeconds()).slice(-2));
			if (format.match(/S/g)) {
				var milliseconds = ('00' + date.getUTCMilliseconds()).slice(-3);
				var length = format.match(/S/g).length;
				for (var i = 0; i < length; i++) {
					format = format.replace(/S/, milliseconds.substring(i, i + 1));
				}
			}
			return format;
		}
	};
	var dateparse = function(str) {
		return utcToLocal(new Date(str));
	};
	var dateformat = function(date, format) {
		return utcFormat(localToUTC(date), format);
	};
	var Datepicker = Input.extend({
		props: {
			value: {
				type: Date,
				default: function() {
					var d = new Date();
					return new Date(d.getFullYear(), d.getMonth(), d.getDate());
				},
			},
		},
		template: '<div class="input-field" >' +
					'<input ' +
						':id="id" ' +
						'ref="input" ' +
						'type="text" ' +
						':placeholder="placeholder" ' +
						':required="required ? \'true\' : null" ' +
						':aria-required="required ? \'true\' : null" ' +
						':readonly="readonly ? \'true\' : null" ' +
						':class="inputClassNames" ' +
						'v-on:input="updateValue($event.target.value)" ' +
						'v-on:blur="onBlur" ' +
						'v-on:focus="onFocus" ' +
						'/>' +
					'<label ref="label" :for="id" >{{label}}</label>' +
				'</div>',
		watch: {
			readonly: function(val) {
				var picker = $(this.$el).find('.datepicker').pickadate('picker');
				if (val) {
					picker.stop();
					this.$refs.input.setAttribute('readonly', true);
				} else {
					picker.start();
					this.$refs.input.removeAttribute('readonly', true);
				}
			},
		},
		methods: {
			valueToDisplay: function(value) {
				if (!value) {
					return '';
				}
				return dateformat(value, 'yyyy/MM/dd');
			},
			inputToValue: function(value) {
				if (!value) {
					return null;
				}
				return dateparse(value);
			},
			blurAdjustInput: function(input) {
				//nop
			},
			focusAdjustInput: function(input) {
				//nop
			},
		},
		mounted: function() {
			var picker = $(this.$el).find('.datepicker').pickadate({
				selectMonths: true, // Creates a dropdown to control month
				selectYears: 15 // Creates a dropdown of 15 years to control year
			}).pickadate('picker');
			picker.on('set', function(e) {
				var d = new Date(e.select);
				if (isNaN(d)) {
					d = null;
				}
				this.$refs.input.value = this.valueToDisplay(d);
				this.$emit('input', d);
			}.bind(this));
			this.$el.appendChild(picker.$root[0]);
			picker.set('select', this.value ? this.value.getTime() : new Date(NaN));

			if (this.readonly) {
				picker.stop();
				this.$refs.input.setAttribute('readonly', true);
			}
		},
		computed: {
			inputClassNames: function() {
				var classNames = Input.options.computed.inputClassNames.call(this);
				classNames.datepicker = true;
				return classNames;
			},
		},
	});

	Vue.component('m-datepicker', Datepicker);
```

### 説明
Input継承して作りました。
あとは[pickadate.js](http://amsul.ca/pickadate.js/)のAPI調べながら頑張ってます。

Dateのフォーマットロジックは[こちら](http://qiita.com/osakanafish/items/c64fe8a34e7221e811d0)参考にさせていただきました。


### 利用

```html:利用側.html
		<div class="row">
			<div class="col s12 center"><h4>Datepickers</h4></div>
			<div class="col s12 m6 l3" style="height: 120px;">
				<m-datepicker label="datepicker" v-model="datepickerValue1" ></m-datepicker>
				{{datepickerValue1 | date.format('yyyy年MM月dd日')}}
			</div>
			<div class="col s12 m6 l3" style="height: 120px;">
				<m-datepicker label="init null value" v-model="datepickerValue2" ></m-datepicker>
				{{datepickerValue2 | date.format('yyyy年MM月dd日')}}
			</div>
			<div class="col s12 m6 l3" style="height: 120px;">
				<m-datepicker required label="required" v-model="datepickerValue3" ></m-datepicker>
				{{datepickerValue3 | date.format('yyyy年MM月dd日')}}
			</div>
			<div class="col s12 m6 l3" style="height: 120px;">
				<m-datepicker readonly label="readonly" v-model="datepickerValue4" ></m-datepicker>
			</div>
		</div>
```

```js
	// new Date(2000, 0, 1) | date.format('yyyy/MM/dd') -> '2000/01/01'
	Vue.filter('date.format', dateformat);

	/**
	 * アプリケーション本体
	 */
	new Vue({
		el: '#main',
		data: {
			datepickerValue1: undefined,
			datepickerValue2: null,
			datepickerValue3: null,
			datepickerValue4: undefined,
		},
	});
```

↑Vue.filter登録してます。

![image](https://qiita-image-store.s3.amazonaws.com/0/84259/a393ff8a-d608-d134-4e3d-0858d2e4d875.png)
![image](https://qiita-image-store.s3.amazonaws.com/0/84259/fe8c7136-ade1-ed61-565e-572117101875.png)
![image](https://qiita-image-store.s3.amazonaws.com/0/84259/3a2055b3-2661-2e53-c60e-9e984e3c9c1a.png)


## Select

```js
	var Select = Vue.extend({
		props: {
			id: String,
			value: {
				type: [String, Number],
				default: null,
			},
			label: String,
			required: Boolean,
			number: Boolean,
			disabled: Boolean,
		},
		template: '<div class="input-field">' +
				'	<select ' +
					':id="id" ' +
					'ref="select" ' +
					':required="required ? \'true\' : null" ' +
					':aria-required="required ? \'true\' : null" ' +
					':disabled="disabled ? \'true\' : null" ' +
					'class="validate" ' +
					'>' +
						'<slot />' +
					'</select>' +
					'<label ref="label" :for="id" >{{label}}</label>' +
				'</div>',
		watch: {
			value: function(newValue, oldValue) {
				var $sel = $(this.$refs.select);
				if ($sel.val() !== newValue) {
					$sel.val(newValue);
					this.materialSelect();
				}
			},
		},
		methods: {
			updateValue: function(value) {
				value = this.selectToValue(value);
				//v-modelが無いとき値が当たらないので設定
				this.$emit('input', value);
				this.validate();
			},
			validate: function() {
				var value = this.selectToValue($(this.$refs.select).val());
				if (this.required && !value) {
					if ($(this.$el).find('select option').length) {
						this.setInvalid('empty');
					} else {
						this.setInvalid('選択可能な値がありません');
					}
					this.reflectInvalid();
					return false;
				}
				this.$refs.select.classList.remove('invalid');
				this.$emit('validate', value, this);
				this.reflectInvalid();
				return this.$refs.select.classList.contains('invalid');
			},
			setInvalid: function(msg) {
				if (msg) {
					this.$refs.label.setAttribute('data-error', msg);
					this.$refs.select.classList.add('invalid');
				}
			},
			getDisplayValue: function() {
				var $sel = $(this.$el).find('select');
				if (-1 === $sel[0].selectedIndex) {
					$sel[0].selectedIndex = 0;
				}
				return $sel.val();
			},
			selectToValue: function(value) {
				if (this.number) {
					return !isNaN(value - 0) ? value - 0 : value;
				}
				return value;
			},
			reflectInvalid: function() {
				//http://stackoverflow.com/questions/34248898/how-to-validate-select-option-for-materialize-dropdown
				$(this.$el).find('.error_note').remove();
				if (this.$refs.select.classList.contains('invalid')) {
					var inputTarget = $(this.$el).find('input.select-dropdown');
					inputTarget.css({
						'border-color': '#EA454B',
						'box-shadow': '0 1px 0 0 #EA454B'
					});
					inputTarget.after(
						'<span class="error_note" style="color: #EA454B;font-size: 12px; position: absolute; top: 49px;">' +
						this.$refs.label.getAttribute('data-error') +
						'</span>'
					);
				}
			},
			materialSelect: function() {
				var $sel = $(this.$refs.select);
				$sel.material_select();
				var inputTarget = $(this.$el).find('input.select-dropdown');
				inputTarget[0]['$ validate $'] = this.validate.bind(this);
			},
		},
		mounted: function() {
			var $sel = $(this.$refs.select);
			$sel.val(this.value);
			//表示内容と違うものが選択されている
			if (this.value !== this.getDisplayValue()) {
				this.value = this.selectToValue(this.getDisplayValue());
			}
			this.materialSelect();

			$sel.on('change', function() {
				this.updateValue($(this.$refs.select).val());
			}.bind(this));

			
		},
		updated: function() {
			var $sel = $(this.$refs.select);
			$sel.val(this.value);
			if (this.value !== this.getDisplayValue()) {
				this.updateValue(this.getDisplayValue());
			}
			this.materialSelect();
			this.reflectInvalid();
		},
	});
	Vue.component('m-select', Select);
```

### 説明
v-modelを反映させる方法はInputと同じような感じです。
`$sel.on('change', function() {`から`this.$emit('input', value);`に伝播するようにしてます。

* validate
    [こちら](http://stackoverflow.com/questions/34248898/how-to-validate-select-option-for-materialize-dropdown)を参考にvalidate出来るようにしてみました。

    ```js
			reflectInvalid: function() {
				//http://stackoverflow.com/questions/34248898/how-to-validate-select-option-for-materialize-dropdown
				$(this.$el).find('.error_note').remove();
				if (this.$refs.select.classList.contains('invalid')) {
					var inputTarget = $(this.$el).find('input.select-dropdown');
					inputTarget.css({
						'border-color': '#EA454B',
						'box-shadow': '0 1px 0 0 #EA454B'
					});
					inputTarget.after(
						'<span class="error_note" style="color: #EA454B;font-size: 12px; position: absolute; top: 49px;">' +
						this.$refs.label.getAttribute('data-error') +
						'</span>'
					);
				}
			},
    ```

* material_select()
    再レンダリングされたとき、`$(this.$refs.select).material_select();`を呼ばないと反映されないので念のため`updated`から呼んでます。

    ```js
		methods: {
			materialSelect: function() {
				var $sel = $(this.$refs.select);
				$sel.material_select();
				//・・・
			},
		//・・・
		updated: function() {
			var $sel = $(this.$refs.select);
			$sel.val(this.value);
			if (this.value !== this.getDisplayValue()) {
				this.updateValue(this.getDisplayValue());
			}
			this.materialSelect();
			//・・・
		},
    ```

    valueが変更されたときも同様に`$(this.$refs.select).material_select();`が必要なのでwatchで呼び出しています。  

    ```js
		watch: {
			value: function(newValue, oldValue) {
				var $sel = $(this.$refs.select);
				if ($sel.val() !== newValue) {
					$sel.val(newValue);
					this.materialSelect();
				}
			},
		},
    ```



### 利用

```html:利用側.html
		<div class="row">
			<div class="col s12 center"><h4>Selects</h4></div>
			<div class="col s12 m6 l3" style="height: 120px;">
				<m-select label="select" v-model="selectValue1" >
					<option value="1">Item1</option>
					<option value="2">Item2</option>
					<option value="3">Item3</option>
					<option value="4">Item4</option>
				</m-select>
				{{selectValue1}}
			</div>
			<div class="col s12 m6 l3" style="height: 120px;">
				<m-select required label="required" v-model="selectValue2" >
					<option value="">Empty</option>
					<option value="1">Item1</option>
					<option value="2">Item2</option>
					<option value="3">Item3</option>
					<option value="4">Item4</option>
				</m-select>
				{{selectValue2}}
			</div>
			<div class="col s12 m6 l3" style="height: 120px;">
				<m-select required label="required empty items" >
				</m-select>
			</div>
			<div class="col s12 m6 l3" style="height: 120px;">
				<m-select number label="number" v-model="selectValue3" >
					<option value="1">Item1</option>
					<option value="2">Item2</option>
					<option value="3">Item3</option>
					<option value="4">Item4</option>
				</m-select>
				{{selectValue3}} : typeof {{typeof selectValue3}}
			</div>
			<div class="col s12 m6 l3" style="height: 120px;">
				<m-select disabled label="disabled" v-model="selectValue4" >
					<option value="1">Item1</option>
					<option value="2">Item2</option>
					<option value="3">Item3</option>
					<option value="4">Item4</option>
				</m-select>
				{{selectValue4}}
			</div>
		</div>
```

```js
	/**
	 * アプリケーション本体
	 */
	new Vue({
		el: '#main',
		data: {
			selectValue1: '1',
			selectValue2: '',
			selectValue3: 3,
			selectValue4: '4',
		},
	});
```

![image](https://qiita-image-store.s3.amazonaws.com/0/84259/ec2b728a-05cc-61c3-399d-0100e43fa6dc.png)

![image](https://qiita-image-store.s3.amazonaws.com/0/84259/c4db7486-f6c7-9c1e-4e05-1566fa707631.png)

## Collection（パート１）
次はCollectionですがベストな方法がわからず悩んでいます。
http://materializecss.com/collections.html

```js
	var Collection = Vue.extend({
		props: {
			items: Array,
			itemComponent: {
				type: Object,
				default: function() {
					return {
						template: '<span>{{ item }} </span>',
						props: {
							item: String,
						},
					};
				},
			},
		},
		//https://github.com/vuejs/vue/issues/2511
		template: '<ul :class="{collection:true, \'with-header\': !!$slots.header}">' +
					'<li v-if="$slots.header" class="collection-header">' +
						'<slot name="header"></slot>' +
					'</li>' +
					'<li class="collection-item" v-for="item in items" >' +
						'<component :is="itemComponent" :item="item"></component>' +
					'</li>' +
				'</ul>',
	});
	Vue.component('m-collection', Collection);
```

### 説明
コンポーネント内部の描画を外から与えてさらにこれを`v-if`で回すには小技が必要です。
下記を見ると`component`タグを使えってことみたいなのでこれは`component`タグで実装しています。
https://github.com/vuejs/vue/issues/2511
https://jsfiddle.net/simplesmiler/5p420nbe/

```js
			itemComponent: {
				type: Object,
				default: function() {
					return {
						template: '<span>{{ item }} </span>',
						props: {
							item: String,
						},
					};
				},
			},
//・・・
					'<li class="collection-item" v-for="item in items" >' +
						'<component :is="itemComponent" :item="item"></component>' +
					'</li>' +
```

この場合、利用側は下記のようになります。

### 利用

```html:利用側.html
		<div class="row">
			<div class="col s12 center"><h4>Collections</h4></div>
			<div class="col s12 m6 l3">
				Normal<br>
				<m-collection :items="collectionItems1" ></m-collection>
			</div>
			<div class="col s12 m6 l3">
				CustomItems<br>
				<m-collection :item-component="$options.components.collectionItem" :items="collectionItems1" ></m-collection>
			</div>
			<div class="col s12 m6 l3">
				Header<br>
				<m-collection :items="collectionItems1" >
					<h4 slot="header">Header</h4>
				</m-collection>
			</div>
			<div class="col s12 m6 l3">
				header2<br>
				<m-collection :items="collectionItems1" >
					<h4 slot="header"><i class="material-icons">grade</i>Header<i class="material-icons">grade</i></h4>
				</m-collection>
			</div>
		</div>
```

```js
	/**
	 * アプリケーション本体
	 */
	new Vue({
		el: '#main',
		data: {
			collectionItems1: [
				'item1',
				'item2',
				'item3',
			],

		},
		components: {
			collectionItem: {
				// script templateも可 `template: '#collectionitem-template',` など
				template: '<div>{{ item }}<a href="#!" class="secondary-content"><i class="material-icons" v-on:click="onClick" >send</i></a></div>',
				props: {
					item: String
				},
				methods: {
					onClick: function() {
						alert('click arrow!!');
					},
				},
			},
		},
		methods: {
			onClickItem: function(item) {
				alert('click!!' + item);
			},
		},
	});
```

![image](https://qiita-image-store.s3.amazonaws.com/0/84259/df8d666d-81ec-3167-84cc-a9236258d6a8.png)

htmlに子要素が書かれてないのが気持ち悪いというか、html見ただけではどうなるのかさっぱりわからないのがいけてない気がします。
ということで次を試しました。

## Collection（パート２）

```js
	var Collection2 = Vue.extend({
		props: {
		},
		render: function(h) {
			var items = [];
			if (this.$slots.header) {
				items.push(this.$createElement('li', {
					class: {
						'collection-header': true,
					}
				}, this.$slots.header));
			}
			if (this.$slots.default) {
				this.$slots.default.forEach(function(slot) {
					if (!slot.tag && (!slot.text || !slot.text.trim())) {
						return;
					}
					//わたってきたslotをliタグで囲む
					items.push(this.$createElement('li', {
						class: {
							'collection-item': true,
						}
					}, [slot]));
				}.bind(this));
			}
			return this.$createElement('ul', {
				class: {
					collection: true,
					'with-header': !!this.$slots.header,
				}
			}, items);
		},
	});
	Vue.component('m-collection2', Collection2);
```

### 説明
templateを使わずrenderという技を使っています。

先に「利用」を見たほうがわかりやすいですが、
`this.$slots.default`に内側に指定されたdom（VNode）のリストが入ってくるので、これでループして構築します。

```js
		render: function(h) {
			var items = [];
			//・・・
			if (this.$slots.default) {
				this.$slots.default.forEach(function(slot) {
					if (!slot.tag && (!slot.text || !slot.text.trim())) {
						return;
					}
					//わたってきたslotをliタグで囲む
					items.push(this.$createElement('li', {
						class: {
							'collection-item': true,
						}
					}, [slot]));
				}.bind(this));
			}
			return this.$createElement('ul', {
				//・・・	
			}, items);
		//・・・
```

`v-for`はコンポーネントではなく呼び出し側で実装してもらいます。




### 利用

```html:利用側.html
		<div class="row">
			<div class="col s12 center"><h4>Collections2</h4></div>
			<div class="col s12 m6 l3">
				Normal<br>
				<m-collection2>
					<span v-for="item in collectionItems1">{{item}}</span>
				</m-collection2>
			</div>
			<div class="col s12 m6 l3">
				CustomItems<br>
				<m-collection2 >
					<div v-for="item in collectionItems1">{{ item }}<a href="#!" class="secondary-content"><i class="material-icons" v-on:click="onClickItem(item)" >send</i></a></div>
				</m-collection2>
			</div>
			<div class="col s12 m6 l3">
				Header<br>
				<m-collection2>
					<h4 slot="header">header</h4>
					<span v-for="item in collectionItems1">{{item}}</span>
				</m-collection2>
			</div>
			<div class="col s12 m6 l3">
				header2<br>
				<m-collection2 >
					<h4 slot="header"><i class="material-icons">grade</i>Header<i class="material-icons">grade</i></h4>
					<div v-for="item in collectionItems1">{{ item }}<a href="#!" class="secondary-content"><i class="material-icons" v-on:click="onClickItem(item)" >send</i></a></div>
				</m-collection2>
			</div>
		</div>
```
![image](https://qiita-image-store.s3.amazonaws.com/0/84259/e0bf0d14-21f6-032a-348e-36e8808d8383.png)

同じ感じになりました。
htmlを見ただけでどういう構造化がわかりやすいという意味では、こちらのほうが良いかもしれません。

さらに下記のようにArrayのデータが無くても利用できるようになります。

```html
				<m-collection2>
					<span >Item1</span>
					<div >Item2<a href="#!" class="secondary-content"><i class="material-icons" v-on:click="onClickItem('item2')" >send</i></a></div>
				</m-collection2>
```

![image](https://qiita-image-store.s3.amazonaws.com/0/84259/0e389ad5-70e6-7759-45ef-bd5ff557c283.png)


ただrender使い始めるとメンテナンスがつらそうな予感はしています。

もうわけわからなくなってきたのでもう一つ。

## Collection（パート３）

```js
	var Collection3 = Vue.extend({
		props: {
		},
		components: {
			renderItem: {
				props: {
					vnode: Object,
				},
				render: function() {
					return this.vnode;
				}
			}
		},
		template: '<ul :class="{collection:true, \'with-header\': !!$slots.header}">' +
					'<li v-if="$slots.header" class="collection-header">' +
						'<slot name="header"></slot>' +
					'</li>' +
					'<li class="collection-item" v-for="vnode in $slots.default"　v-if="vnode.tag || (vnode.text && vnode.text.trim())" >' +
						'<render-item :vnode="vnode" ></render-item>' +
					'</li>' +
				'</ul>',
	});
	Vue.component('m-collection3', Collection3);
```

### 説明
パート２と同じ動きのはずです。
renderでの実装範囲を小さくしてみました。VNodeを描画してくれるコンポーネントを内部用に作成して対応しました。
これはさすがにVuejsのバージョンアップ等ですぐに殺されそうです。

### 利用


```html:利用側.html
		<div class="row">
			<div class="col s12 center"><h4>Collections3</h4></div>
			<div class="col s12 m6 l3">
				Normal<br>
				<m-collection3>
					<span v-for="item in collectionItems1">{{item}}</span>
				</m-collection3>
			</div>
			<div class="col s12 m6 l3">
				CustomItems<br>
				<m-collection3 >
					<div v-for="item in collectionItems1">{{ item }}<a href="#!" class="secondary-content"><i class="material-icons" v-on:click="onClickItem(item)" >send</i></a></div>
				</m-collection3>
			</div>
			<div class="col s12 m6 l3">
				Header<br>
				<m-collection3>
					<h4 slot="header">header</h4>
					<span v-for="item in collectionItems1">{{item}}</span>
				</m-collection3>
			</div>
			<div class="col s12 m6 l3">
				header2<br>
				<m-collection3 >
					<h4 slot="header"><i class="material-icons">grade</i>Header<i class="material-icons">grade</i></h4>
					<div v-for="item in collectionItems1">{{ item }}<a href="#!" class="secondary-content"><i class="material-icons" v-on:click="onClickItem(item)" >send</i></a></div>
				</m-collection3>
			</div>
		</div>
```

![image](https://qiita-image-store.s3.amazonaws.com/0/84259/0dfc7441-ec15-9540-7754-ee18da2a757e.png)

# 実物  
今回のソースはGitHubにアップしてghpagesで動かせるようにしています。
## DEMO
https://ota-meshi.github.io/vue_components_example_materialize/
## GitHub
https://github.com/ota-meshi/vue_components_example_materialize