Vue.jsのTodoサンプルをコンポーネント化してTypeScriptで書く

[Vue.js](https://jp.vuejs.org/guide/index.html)を使う機会が出てきそうなので、最近Angular2から浮気して勉強中。
でも、Angular2でTypeScriptで書くことに慣れてしまったから、Vue.jsもTypeScriptで書きたい。
そこで、TypeScriptでVue.js(特にコンポーネント)を書く方法についていろいろ調べてみました。
Vue.jsのバージョンとしては1.0.28を使っています。
最終的にはVue.jsのサイトに載ってるTodoのサンプルをコンポーネント化して、TypeScriptで書くところまでやってみます。

# Vue.jsとTypeScript
## 相性が悪い
いろいろ調べていくと、Vue.jsとTypeScriptは相性が悪いそうです。
というか、Angular2のTypeScript親和性がすごすぎる感じはしてます。
まあ、Angular2の場合、もともとの設計思想がTypeScriptで書けるようにというのもあるんでしょうが。
閑話休題。Vue.jsでコンポーネントを作る場合はVue.extendに私引数オブジェクトのプロパティとしてデータやメソッドを定義していきます。
それらをVue.jsが内部で処理することでコンポーネント化していますが、このような場合コンパイラがうまく型チェックや補完を行うことがができません。
そんな事情もあり、型定義ファイルを入れてTypeScriptである程度書くことはできても、いまいちTypeScriptの恩恵を得ることができないのが実情のようです。

## ライブラリを使う
そんな相性の悪さを解消してTypeScriptで記述可能にしてくれるライブラリは実はいろいろ出ています。

-  vue-class-component
-  vue-typescript
-  vueit

おそらく一番有名なんではないかと思うのが[vue-class-component](vue-class-component)です。
Babel(ES7)やTypeScriptのデコレータでVue.jsのコンポーネントを記述できるライブラリです。
最初はこれを使おうとしたんですが、私の環境では`Cannnot find module 'vue'`エラーが出てしまいました。型定義をうまく読み込めてない感じがします。
一応、エラーは出てもコンパイルは通ってjsは生成されるみたいなんですけどね。
公式のexampleの中とか見るとjsで書いてるので、Babel(ES7)向けってことなのかもしれません。

vueitは今回参考にさせていただいている[この記事](http://qiita.com/wonderful_panda/items/3776896acc5faafe7a03)の投稿者の方がvue-class-componentとvue-typescriptの不満なところを解消するために作ったライブラリとのこと。
ただ、構文を見る限りだとなんとなくvue-class-componentのほうが直観的にわかりやすそうだったので、今回はこちらは使ってないです。

## vue-typed
で、結局どうしたかというと、npmの公開パッケージを見て行ってよさそうなものを見つけました。
[vue-typed](https://github.com/budiadiono/vue-typed)です。
説明を読むと、TypeScript用のvue-class-componentという感じのようです。
上述の`Cannnot find module 'vue'`エラーが解消したので、これを採用しました。
次の項目からは実際にvue-typedを使いながらTodoコンポーネントを作成していきます。

#　TypeScriptでTodoコンポーネント
## 準備
まずは事前準備としてnpmやTypeScript、Webpack、typings、gulpなどの環境を構築しておきましょう。
その上で次のコマンドを実行し、Vue.js本体とvue-typedを入れます。
また、typingsでVueの型定義ファイルも入れておきます。

```
npm i vue --save
npm i vue-typed --save-dev
typings install -G dt~vue
```

## サンプルを動かす
今回対象とするサンプルをまずは動かしましょう。
[ここ](https://jp.vuejs.org/guide/index.html#All-Together-Now-TODO-アプリケーション)のコードを使います。
部分的にしか乗ってないので、scriptの読み込みコードなどを追加して動かせる状態にします。

```html:index.html
<!DOCTYPE html>
<html>
    <head>
      <meta charset="utf-8">
      <title>VueJSTest</title>
    </head>
    <body>
      <div id="app">
        <input v-model="newTodo" v-on:keyup.enter="addTodo">
        <ul>
          <li v-for="todo in todos">
            <span>{{ todo.text }}</span>
            <button v-on:click="removeTodo($index)">X</button>
         </li>
       </ul>
      </div>
    </body>
    <script src="./node_modules/vue/dist/vue.js"></script>
    <script src="./bundle.js"></script>
</html>
```

```js:bundle.js
new Vue({
  el: '#app',
  data: {
    newTodo: '',
    todos: [
      { text: 'Add some todos' }
    ]
  },
  methods: {
    addTodo: function () {
      var text = this.newTodo.trim()
      if (text) {
        this.todos.push({ text: text })
        this.newTodo = ''
      }
    },
    removeTodo: function (index) {
      this.todos.splice(index, 1)
    }
  }
})
```

Todoアプリが動けばOK。
![image](https://qiita-image-store.s3.amazonaws.com/0/115996/b05b02ce-8660-421b-1986-bf8c96220028.png)
挙動を確かめたい方はリンク先の下の方に実際のコンポーネントがあるので確かめてみましょう。

## コンポーネント化する
TypeScriptで書く前にjavascriptのままでコンポーネント化します。

```html:index.html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>VueJSTest</title>
</head>
  <body>
    <div id="app">
      <todo>
      </todo>
    </div>
  </body>
  <script src="./node_modules/vue/dist/vue.js"></script>
  <script src="./bundle.js"></script>
</html>
```

```js:index.js
var TodoComponent = Vue.extend({
  template:`
  <input v-model="newTodo" v-on:keyup.enter="addTodo">
  <ul>
      <li v-for="todo in todos">
          <span>{{ todo.text }}</span>
          <button v-on:click="removeTodo($index)">X</button>
      </li>
  </ul>`,
  data: function(){
    return {
      newTodo: '',
      todos: [
        { text: 'Add some todos' }
      ]
    };
  },
  methods: {
    addTodo: function () {
      var text = this.newTodo.trim();
      if (text) {
        this.todos.push({ text: text });
        this.newTodo = '';
      }
    },
    removeTodo: function (index) {
      this.todos.splice(index, 1);
    }
  }
});
// コンポーネント登録
Vue.component("todo", TodoComponent);
// root インスタンスを作成
new Vue({
  el: "#app"
});
```

TodoComponentというコンポーネントを作ってそれをtodoタグに登録します。
html側に書いたtodoタグの場所にTodoComponentが表示されます。
ただし、Vueのルートが`#app`であるため、`id=app`のdivの外にtodoタグを書いても意味がないです。

## TypeScriptで書く
いよいよ、TypeScriptでTodoComponentを書きます。
HTMLのほうはコンポーネント化後のHTMLをそのまま使うので、変更の必要はないです。

```ts:index.ts
///<reference path="./typings/index.d.ts"/>
import { Component, Data } from "vue-typed";

@Component({
  template: `
  <input v-model="newTodo" v-on:keyup.enter="addTodo">
  <ul>
      <li v-for="todo in todos">
          <span>{{ todo.text }}</span>
          <button v-on:click="removeTodo($index)">X</button>
      </li>
  </ul>`
})
class TodoComponent {
  @Data()
  newTodo:string =  "";
  @Data()
  todos: any[] = [
    { text: "Add some todos" }
  ];
  addTodo() {
    let text = this.newTodo.trim();
    if (text) {
      this.todos.push({ text: text });
      this.newTodo = "";
    }
  }
  removeTodo (index) {
    this.todos.splice(index, 1);
  }
};
// コンポーネント登録
Vue.component("todo", TodoComponent);
// root インスタンスを作成
new Vue({
  el: "#app"
});
```
まず、`///<reference path="./typings/index.d.ts"/>`でtypingsの型定義ファイルを読み込みます。
コンポーネントの作成自体はvue-typedだけで事足りますが、コンポーネントの登録やVueのrootインスタンス作成の時にVueへのアクセスが必要であり、そのままでは型エラーが発生するので読み込んでいます。
逆にいえば、型定義ファイルはコンポーネント登録とrootインスタンス登録を行うエントリポイントとなるtsファイルだけで読み込みを行うだけでいいということです。

その後、vue-typedの`Component`デコレータを使って記述します。
`template`はデコレータの中に書き、`data`、`methods`に記述していたものはそれぞれクラスのフィールド、メソッドとして定義し直します。
ただし、data(フィールド)は`@Data()`を使って書く必要があります。

ここまで書いて、webpackなどでbundle.jsにコンパイルすればTodoが動作します。

## 他のデコレータ
ちらっと見た限りですが、vue-typedには6つデコレータがあるようです。

- Component
- Data
- Getter
- Action
- Props
- Watch

今回、ComponentとDataは使いました。PropsとWatchはVue.jsのprops、watchオプションを記述するためのデコレータのようです。
GetterとActionは調査中。

#参考
[TypescriptでVue componentを書くためのライブラリを作った](http://qiita.com/wonderful_panda/items/3776896acc5faafe7a03)