Vue.js の主要な機能をざっくりとつかってみたときのメモ

久しぶりにWEB開発しようとしたら、2-3年前にやってた AngularJS + Grunt + Bower + Yeoman な構成はいまや古い部類の構成になってるようで、、いま時点(2018/12)で、似た感じかつシンプルなフレームワークを探してて [Vue.js](https://jp.vuejs.org/index.html) にたどり着きました。

ってことで、ちょっとさわってみたときの作業メモ。

WEB系のフレームワークで必ずでてくる、TODOリストの構築を行ってみましょう。

ちなみに2019/01/28時点:

- [Vue.js の主要な機能をざっくりとつかってみたときのメモ](https://qiita.com/masatomix/items/24142b084b745ce816a9) 本記事
- [Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase編)](https://qiita.com/masatomix/items/7be14b8eb433edcfbfc7) 次の記事
- [Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase認証・認可編)](https://qiita.com/masatomix/items/5316acdb5980289e5cc2) さらにその次

これだけQiita化しています。


## 前提環境
今回作業している環境です。

```bash
$ sw_vers
ProductName:	Mac OS X
ProductVersion:	10.14.2
BuildVersion:	18C54
$ node --version
v10.15.0
$ npm --version
6.4.1
$
```

## インストール

vue-cli というVue.jsのテンプレを生成してくれるコマンドラインツールをインストールします。

```bash
$ npm install -g vue-cli
$ source ~/.bash_profile
$ vue --version
2.9.6
```
よさそうですね。


## やってみる

### プロジェクト作成


```bash 
$ vue init webpack todo-examples

↓とりあえずすべて Yes

? Project name todo-examples
? Project description A Vue.js project
? Author Masatomi KINO <kino@example.com>
? Vue build standalone
? Install vue-router? Yes
? Use ESLint to lint your code? Yes
? Pick an ESLint preset Standard
? Set up unit tests Yes
? Pick a test runner jest
? Setup e2e tests with Nightwatch? Yes
? Should we run `npm install` for you after the project has been created? (recom
mended) npm

   vue-cli · Generated "todo-examples".

# Installing project dependencies ...
# ========================
...

# Project initialization finished!
# ========================

To get started:

  cd todo-examples
  npm run dev

Documentation can be found at https://vuejs-templates.github.io/webpack
```

完了しました。

### Hello World

最後に`` cd todo-examples``,``npm run dev`` とでてましたので、その通りに。。

```bash
$ cd todo-examples
$ npm run dev
...
 DONE  Compiled successfully in 7650ms                                                                  15:42:24

 I  Your application is running here: http://localhost:8080
```

さて、メッセージ通りにブラウザで http://localhost:8080 をひらいてみます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/6f92ac9e-97d9-14af-c221-99d3b5ce15a6.png)

よさそうですね。。

## TIPS

### v-model でデータバインディング

まずはデータバインディングから。 HelloWorld.vue を以下のようにします。


```vuejs:HelloWorld.vue
<template>
  <div class="hello">
    <p><input type='text' v-model='msg'></p> <!-- このへん -->
    <p>{{msg}}</p>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      msg: 'Hello World.'
    }
  }
}
</script>
```

ブラウザをリロードしてみると(たぶん勝手にリロードされてます)テキストボックスが表示されると思いますが、 その v-modelが設定されたテキストボックスに値を入力すると、``{{msg}}``部分も値が更新されると思います。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/49ba1073-f9c3-0b57-2774-ae0ba6236d6c.png)



### 配列データを v-forで繰り返し,submit ボタンでメソッド呼び出し
つづいて配列をHTML上の v-forタグで走査したり、``v-on:submit``もしくはその省略形の``@submit``でイベントハンドラを登録したりしてみます。

イベントハンドラとして登録する、画面で使用するメソッドは、``methods`` に定義していきます。

また、Vue.jsはその画面上で使用する変数を``data()``で定義するようになっていて、todos配列には初期値をセットしてあります。

結果、HelloWorld.vue を以下の通りにしましょう。


```vuejs:HelloWorld.vue
<template>
  <div class="hello">
    <ul>
      <li v-for='todo in todos' >{{todo}}</li> <!-- 繰り返し処理 -->
    </ul>
    <form @submit.prevent='addItem'> <!-- submitされたらaddItemメソッドを呼ぶ -->
      <input type='text' v-model='newTask'>
      <input type='submit' value='追加' >
    </form>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () { // 画面で使用する変数を定義する場所
    return {
      newTask:'',
      todos : [
        { name: "task 1", isDone: false },
        { name: "task 2", isDone: false },
        { name: "task 3", isDone: false },
        { name: "task 4", isDone: false }   
      ]
    }
  },
  methods:{ // 画面やscriptから呼び出すメソッド
    addItem: function(){
      this.todos.push({
        name: this.newTask,
        isDone: false
      })
      this.newTask=''
    }
  }
}
</script>
```

``data()`` で、``newTask``と``todos`` という変数を定義しました。また``methods``では、追加ボタンをクリックしたときに呼び出す ``addItem``メソッドを定義しました。

ちなみにそのメソッド内では、``this.newTask``などと``「this」``をつけることで、``data()``で定義した変数にアクセスできます。

以上で、追加ボタンをクリックするとformのsubmitが行われて、addItemメソッドが呼び出された結果、タスクが追加されるようになりました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/87575484-5773-5445-22ab-2d58344cb114.png)


### v-bind:classでクラスの制御、computedで計算処理

さてつぎは、v-bindです。下記のように li をv-forしている箇所に

```vuejs:HelloWorld.vue(抜粋)
    <ul>
      <li v-for='(todo,index) in todos' >
        <input type='checkbox' v-model='todo.isDone'>
        <span v-bind:class='{done: todo.isDone}'>{{todo.name}}</span>
        <span @click='deleteTask(index)' class='xButton'>[x]</span>
        </li>
    </ul>
```

って``<span v-bind:class='{done: todo.isDone}'>...</span>`` と書くことで、todo.isDoneのときだけ(タスクが終わってるときだけ) class属性に doneがセットされるようになります。そしてcssで、そのクラスは完了線を引くようにしています。

チェックボックスは、``v-model``をつかって ``todo.isDone``に連動させています。


また

```vue
<span @click='deleteTask(index)' class='xButton'>[x]</span>
```

として、クリックしたときのメソッド ``deleteTask`` を追加しています。引数の``「index」``は、v-for を ``<li v-for='(todo,index) in todos' >``と記述することで、index番号を使用可能にしています。


さいごに``data()``と``methods``のならびに、``computed``部を追加し、そこにメソッド``remainingTask`` を定義しています。``computed``に定義したメソッドは、画面上から``{{remainingTask}}``とやって呼び出すことができます。

まとめると、全体はこんな感じ。

```vuejs:HelloWorld.vue
<template>
  <div class="hello">
    <h1>My Todo Task<span class='info'>({{remainingTask.length}}/{{todos.length}})</span></h1>
    <ul>
      <li v-for='(todo,index) in todos' >
        <input type='checkbox' v-model='todo.isDone'>
        <span v-bind:class='{done: todo.isDone}'>{{todo.name}}</span>
        <span @click='deleteTask(index)' class='xButton'>[x]</span>
        </li>
    </ul>
    <form @submit.prevent='addTask'>
      <input type='text' v-model='newTask'>
      <input type='submit' value='追加' >
    </form>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      newTask:'',
      todos : [
        { name: "task 1", isDone: true },
        { name: "task 2", isDone: false },
        { name: "task 3", isDone: false },
        { name: "task 4", isDone: false }   
      ]
    }
  },
  methods:{
    addTask: function(){
      this.todos.push({
        name: this.newTask,
        isDone: false
      })
      this.newTask=''
    },
    deleteTask: function(index){
      this.todos.splice(index,1)
    }
  },
  computed:{
    remainingTask: function(){
      return this.todos.filter(function(todo){
        return !todo.isDone
      })
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1 {
  font-size: 16px;
  border-bottom: 1px solid #ddd;
  padding: 16px 0;
}

.xButton{
  cursor: pointer;
  font-size: 12px;
  color: red
}

li > span.done{
  text-decoration: line-through;
  color:#bbb
}

.info {
  color: #bbb;
  font-size: 12px;
}
</style>
```

ブラウザを見てみると、、、

![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/b2a197bd-d56f-b5af-3902-70e6ed697854.png)


チェックボックスと削除ボタンが追加され、チェックを入れると``v-model``でバインドした``todo.isDone``がtrueとなり、完了線がつきます。

削除ボタンをクリックすると、``deleteTask``が呼ばれて該当タスクが削除されます。

よさそうです。


### ボタンで全削除

つぎに「完了タスク全削除」って機能を追加します。すぐうえで``computed``に
 ``remainingTask`` メソッドを追加ましたが、内容は「いま未完了のタスクを返す」ようにしてあるので、``todos``をそれに置き換えるだけですね。

```vuejs:HelloWorld.vue
<template>
  <div class="hello">
    <h1>
      <button @click="deleteEndTask">完了タスクの削除</button>My Todo Task<span class='info'>({{remainingTask.length}}/{{todos.length}})</span></h1>
     ... 省略
</template>

<script>
export default {
  ... 省略
  methods:{
    ... 省略
    deleteEndTask:function(){
      this.todos = this.remainingTask;
    }
  },
  ... 省略
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
...省略
li > span.done{
  text-decoration: line-through;
  color:#bbb
}
</style>
```


### LocalStorageからの読み書き、watchによる変数の監視。

つづいて、リロードしても消えないように画面初期化時に``localStorage``から読み込んでみたり、画面から追加や削除など``todos``に対しての操作を監視し、``todos``に変更があったときに``localStorage``へ保存したり、してみます。

変数の監視は、``watch`` をつかって実装します。``watch``部に

```javascript:HelloWorld.vue(抜粋)
watch:{
 todos: {
   handler: function() {
     localStorage.setItem('todos',JSON.stringify(this.todos))
   },
   deep: true
 }
}
```
と記述することで該当の変数の変更を検知することが出来ます。

したがって、``todos``変数を監視し、変更を検知したらStorageに書き込むようにしました。
また、``created``部は、起動時に呼び出される関数なので、そこで

```javascript:HelloWorld.vue(抜粋)
created: function() {
  this.todos = JSON.parse(localStorage.getItem("todos")) || [];
}
```

として、Storageから読み込むか、読めなければカラの配列をセットします。

変更した差分は以下の通りです。

```javascript:HelloWorld.vue(抜粋)
<script>
export default {
   name: 'HelloWorld',
   data () {
     return {
       newTask:'',
-      todos : [
-        { name: "task 1", isDone: true },
-        { name: "task 2", isDone: false },
-        { name: "task 3", isDone: false },
-        { name: "task 4", isDone: false }   
-      ]
+      todos : []
     }
   },
+  watch: {
+    todos: {
+      handler: function() {
+        localStorage.setItem('todos',JSON.stringify(this.todos))
+      },
+      deep: true
+    }
+  },
+  created: function() {
+    this.todos = JSON.parse(localStorage.getItem("todos")) || [];
+  },
   methods:{
     addTask: function(){
       this.todos.push({
```

全体はこんな感じになりました


```vuejs:HelloWorld.vue
<template>
  <div class="hello">
    <h1>
      <button @click="deleteEndTask">完了タスクの削除</button>My Todo Task<span class='info'>({{remainingTask.length}}/{{todos.length}})</span></h1>
    <ul>
      <li v-for='(todo,index) in todos' >
        <input type='checkbox' v-model='todo.isDone'>
        <span v-bind:class='{done: todo.isDone}'>{{todo.name}}</span>
        <span @click='deleteTask(index)' class='xButton'>[x]</span>
        </li>
    </ul>
    <form @submit.prevent='addTask'>
      <input type='text' v-model='newTask'>
      <input type='submit' value='追加' >
    </form>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {
      newTask:'',
      todos : []
    }
  },
  watch: {
    todos: {
      handler: function() {
        localStorage.setItem('todos',JSON.stringify(this.todos))
      },
      deep: true
    }
  },
  created: function() {
    this.todos = JSON.parse(localStorage.getItem("todos")) || [];
  },
  methods:{
    addTask: function(){
      this.todos.push({
        name: this.newTask,
        isDone: false
      })
      this.newTask=''
    },
    deleteTask: function(index){
      this.todos.splice(index,1)
    },
    deleteEndTask:function(){
      this.todos = this.remainingTask;
    }
  },
  computed:{
    remainingTask: function(){
      return this.todos.filter(function(todo){
        return !todo.isDone
      })
    }
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1 {
  font-size: 16px;
  border-bottom: 1px solid #ddd;
  padding: 16px 0;
}

h1 > button {
  float: right;
}

.xButton{
  cursor: pointer;
  font-size: 12px;
  color: red
}

li > span.done{
  text-decoration: line-through;
  color:#bbb
}

.info {
  color: #bbb;
  font-size: 12px;
}
</style>
```

ブラウザで開いたのち、追加・完了・削除などを行ってからリロードしてみたり、ブラウザを閉じて開いたりしてみて、タスクが正しく保存されていることを確認してみてください。


### コンポーネントをつくる
いまのところtodoの一画面があるだけですが、たくさん画面を作ったら、共通のヘッダやフッタをつけたくなりますよね。というわけで、ヘッダのコンポーネント(Header.vue) を作成し、App.vue から参照するようにします。

```vuejs:components/Header.vue(新規)
<template>
  <header>{{message}}</header>
</template>

<script>
export default {
  name: 'Header',
  data () {
    return {
      message:'共通ヘッダ'
    }
  },
  computed:{
  }
}
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
```

```vuejs:App.vue(修正)
<template>
  <div id="app">
    <Header /> <!-- 追加 --> 
    <router-view/>
  </div>
</template>

<script>
import Header from '@/components/Header'  // コンポーネントをimportして

export default {
  name: 'App',
  components:{
    Header // つかうことを宣言する
  }
}
</script>

<style>
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  /* text-align: center; */
  color: #2c3e50;
  margin-top: 60px;
}
</style>

```

ブラウザを開いてみると、
![image.png](https://qiita-image-store.s3.amazonaws.com/0/73777/3951f8a3-be83-519f-92cd-11bb018435f4.png)
よさそうですね。このヘッダはApp.vueに設置されているので、すべての画面に表示されるようになります。


## いったんまとめ

いやあ、Vue.js よいですね。AngularJSやってた身からすると、作者がおなじだからか今のところほぼおなじといえる使い勝手です。逆にいうと数年前のAngularJSと違いがまだ体感できてない状態です、、orz。


いまのところドットインストールのVue.js入門とほぼおなじ仕組みになっちゃったので、ひきつづきFirebaseやVuexなどとの連携を整理してみようと思います。


## ソースコード
https://github.com/masatomix/todo-examples

上記はマスタブランチで、この時点でのソースはこちらです。
https://github.com/masatomix/todo-examples/tree/for_qiita_init_001

下記手順でビルドできます。

```bash
$ git clone --branch for_qiita_init_001 https://github.com/masatomix/todo-examples.git
$ cd todo-examples/
$ npm install
$ npm run dev
```

これで、http://localhost:8080/  にブラウザでアクセスできるとおもいます。

おつかれさまでした。。

## 関連リンク
- [Vue.js](https://jp.vuejs.org/v2/guide/index.html) 公式
- [基礎から学ぶ Vue.js](https://cr-vue.mio3io.com/tutorials/todo.html) 
- [Vue.js入門](https://dotinstall.com/lessons/basic_vuejs_v2) 

- [Vue.js の主要な機能をざっくりとつかってみたときのメモ](https://qiita.com/masatomix/items/24142b084b745ce816a9) 本記事
- [Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase編)](https://qiita.com/masatomix/items/7be14b8eb433edcfbfc7) 次の記事
- [Vue.js の主要な機能をざっくりとつかってみたときのメモ(Firebase認証・認可編)](https://qiita.com/masatomix/items/5316acdb5980289e5cc2) さらにその次