Vue.jsとRailsでTODOアプリのチュートリアルみたいなものを作ってみた

# はじめに
業務で使っているわけではないのですが、個人的にコツコツVue.jsの勉強をしています。

今回は今まで勉強したことを整理する意味合いも兼ねて、チュートリアルのようなものを作成したいと思います。
見ていただいた方の参考になれば嬉しいです。
また、JavaScriptに関しては書き慣れていないので、もしもっと良い書き方などありましたらご意見いただけると幸いです。

なお、テストコードはまだ書けておりません。。。
次回の記事で各コンポーネントのユニットテストを書きたいと思っています。
**2017/09/20 追記**
試しに書いたテストコードの差分を、記事の最後に追記しました。

## 作るもの
簡単なTODOアプリです。
（どっかのサービスに似ていると言わないでください。。。）

![sample_todo_spa_2.gif](https://qiita-image-store.s3.amazonaws.com/0/118817/8358c9be-47fb-a1b9-efe5-0c42b9d6e2e0.gif)


TODOの管理はRailsのAPIで実施します。
## 前提条件及び環境
Ruby、Rails、Node.jsの環境をご用意ください。
下記の記事などが分かりやすいです。

* [rbenvを利用したRuby環境の構築](http://dev.classmethod.jp/server-side/language/build-ruby-environment-by-rbenv/)
* [Node.jsとnpm インストールとアップデート](http://qiita.com/jaxx2104/items/2277cec77850f2d83c7a)

また、Webpack、およびVue.jsは`yarn`でインストールされるため、`yarn`もインストールしておいてください。
[yarnを使ってみた](http://qiita.com/masterkey1009/items/50f95b1187646a7db385)

本記事を書くにあたり、下記の環境で実施しました。

* Ruby 2.4.1
* Ruby on Rails 5.1.4
* Node.js 8.5.0
* yarn 1.0.2
* Webpacker 3.0.2
* Vue.js 2.4.3

## Webpackについて
複数のJSファイルをひとまとめにするモジュールバンドラーのことです。
複数ファイルに分割して管理しつつ、サーバーへのリクエスト数を削減することができるようです。
[最新版で学ぶwebpack 3入門 – JavaScript開発で人気のバンドルツール](https://ics.media/entry/12140)
# 準備
まずはもろもろインストールをしていきます。
なお、こちらの記事が動画つきで非常に分かりやすいので、もし初めて実施される方はご覧になった方が良いかと思います。
[【動画付き】Rails 5.1で作るVue.jsアプリケーション ～Herokuデプロイからシステムテストまで～](http://qiita.com/jnchito/items/30ab14ebf29b945559f6)

## Rails + Vue.jsのプロジェクトを作成する
はじめから`--webpack`オプションを使用してプロジェクトを作成します。

```
$ rails new todo_sample --webpack=vue
```
`rails s`でRailsのウェルカムページが表示されれば大丈夫です。
## Vue.jsの表示確認
基本的に、Railsで用意するビューファイルは1つのみで、そこを差し替えていきます。
まずは、以下のファイルを作成、編集します。

* `app/controllers/home_controller.rb`
* `config/routes.rb`
* `app/views/home/index.html.erb`

```rb:app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
  end
end
```
```rb:config/routes.rb
Rails.application.routes.draw do
  root to: 'home#index'
end
```
```rb:app/views/home/index.html.erb
<%= javascript_pack_tag 'hello_vue' %>
```
`javascript_pack_tag`を使用することで、`app/javascript/packs`以下にあるJSファイルを探してくれます。
インストール時に`hello_vue.js`というファイルが生成されているので、これを`index`にて読み込ませます。
これで`rails s`して、「Hello Vue!」と表示されれば大丈夫です。
## devサーバーを設定する
Vue.js（というよりWebpack）関連のファイルは変更したらコンパイルする必要があります。
コンパイルには、`bin/webpack`というコマンドを使用する必要があります。

ただ、毎回コンパイルするのは面倒なので、変更を検出して自動コンパイルするようにします。
こちらの記事でも紹介されております。
[Introducing Webpacker](https://medium.com/statuscode/introducing-webpacker-7136d66cddfb)

まずは`foreman`のGemをインストールします。
`foreman`は`Procfile`から複数のプロセスを管理することができます。
[https://github.com/ddollar/foreman](https://github.com/ddollar/foreman)

```diff:Gemfile
+ gem 'foreman'
```
これで`bundle install`します。
次に下記2点のファイルを作成します。

| ファイル名 | 役割 |
|:----------------:|:-----------------:|
| bin/server | Procfile.devのコマンドを実行する |
| Procfile.dev | `rails s`と`bin/webpack-dev-server`を実行する |

```sh:bin/server
#!/bin/bash -i
bundle install
bundle exec foreman start -f Procfile.dev
```
```sh:Procfile.dev
web: bundle exec rails s
# watcher: ./bin/webpack-watcher
webpacker: ./bin/webpack-dev-server
```
また、`bin/server`のパーミッションを変更しておきます。

```
$ chmod 777 bin/server
```
これで`bin/server`を実行してみると、`http://localhost:5000`で起動します。
（ポート番号が変わります。）
ここで、`app.vue`の下記の部分を変えて保存すると、コンパイル処理が走り、画面がリロードされるはずです。
適当に変えて遊んでみて、問題なさそうなら大丈夫です。

```js:app/javascript/packs/app.vue
<script>
export default {
  data: function () {
    return {
      // この文字列が画面に表示されている
      message: "Hello Vue!"
    }
  }
}
</script>
```
# APIの準備
サーバーサイドのAPI部分を実装していきます。
## テーブルとモデルの生成
テーブル名は`tasks`とし、下記のようにします。

| カラム | 型 |  |
|:----------------:|:-----------------:|:------------------|
| name | VARCHAR(255) | NULL: false |
| is_done | BOOLEAN | NULL: false, DEFAULT: false |
| created_at | DATETIME | NULL: false |
| updated_at | DATETIME | NULL: false |

マイグレーションファイルとモデルは`rails generate`で作成してしまいます。

```
$ rails generate model Task name:string is_done:boolean
```
マイグレーションファイルに`null: false`と`default: false`を追記するため、下記のように編集します。

```rb:db/migrate/xxxxxxxxxxxxxx_create_tasks.rb
class CreateTasks < ActiveRecord::Migration[5.1]
  def change
    create_table :tasks do |t|
      t.string :name, null: false
      t.boolean :is_done, default: false, null: false

      t.timestamps
    end
  end
end
```
マイグレーションを実行します。

```
$ rails db:migrate
```
ついでに、モデルの`name`プロパティにバリデーションをつけておきましょう。

```diff:app/models/task.rb
class Task < ApplicationRecord
+ validates :name, presence: true
end
```
## コントローラーと返却するJSONファイルを生成
アクセスするURLは`/api/tasks`のように名前空間を切りたいと思います。
まずはルーティングからです。

```diff:config/routes.rb
Rails.application.routes.draw do
  root to: 'home#index'

+ namespace :api, format: 'json' do
+   resources :tasks, only: [:index, :create, :update]
+ end
end
```
次にコントローラーを作成します。
コントローラーは`app/controllers`の中に`api`というディレクトリを作成し、そこに作ります。

```rb:app/controllers/api/tasks_controller.rb
class Api::TasksController < ApplicationController

  # GET /tasks
  def index
    # 後々のため、更新順で返します
    @tasks = Task.order('updated_at DESC')
  end

  # POST /tasks
  def create
    @task = Task.new(task_params)

    if @task.save
      render :show, status: :created
    else
      render json: @task.errors, status: :unprocessable_entity
    end
  end

  # PATCH/PUT /tasks/1
  def update
    @task = Task.find(params[:id])
    if @task.update(task_params)
      render :show, status: :ok
    else
      render json: @task.errors, status: :unprocessable_entity
    end
  end

  private
    # Never trust parameters from the scary internet, only allow the white list through.
    def task_params
      params.fetch(:task, {}).permit(
          :name, :is_done
      )
    end
end
```
ビューのJSONファイルも同様に、`views/api/tasks`以下に作成します。

```rb:app/views/api/tasks/index.json.jbuilder
json.set! :tasks do
  json.array! @tasks do |task|
    json.extract! task, :id, :name, :is_done, :created_at, :updated_at
  end
end
```
```rb:app/views/api/tasks/show.json.jbuilder
json.set! :task do
  json.extract! @task, :id, :name, :is_done, :created_at, :updated_at
end
```
## seeds.rbを作成し、curlコマンドで確認
`seeds.rb`を作成し初期データを作成できるようにします。
もしレコードが増えすぎてもこれでやり直しもできます。

```rb:db/seeds.rb
3.times { Task.create!(name: 'Sample Task') }
2.times { Task.create!(name: 'Sample Task', is_done: true) }
```
DBに適用します。

```
$ rails db:seed
```
なお、リセットする場合は、

```
$ rails db:setup
```
`curl`コマンドを使用してAPIの確認をします。

```
$ curl localhost:5000/api/tasks
{"tasks":[{"id":1,"name":"Sample Task","is_done":false,"created_at":"2017-09-14T08:12:35.454Z","updated_at":"2017-09-14T08:12:35.454Z"},{"id":2,"name":"Sample Task","is_done":false,"created_at":"2017-09-14T08:12:35.460Z","updated_at":"2017-09-14T08:12:35.460Z"},{"id":3,"name":"Sample Task","is_done":false,"created_at":"2017-09-14T08:12:35.462Z","updated_at":"2017-09-14T08:12:35.462Z"},{"id":4,"name":"Sample Task","is_done":true,"created_at":"2017-09-14T08:12:35.468Z","updated_at":"2017-09-14T08:12:35.468Z"},{"id":5,"name":"Sample Task","is_done":true,"created_at":"2017-09-14T08:12:35.475Z","updated_at":"2017-09-14T08:12:35.475Z"}]}
```
次にPOSTで新規作成してみます。
ここで恐らくエラーになるかと思います。

```
$ curl -X POST localhost:5000/api/tasks -d 'task[name]=fugafuga'
```
エラー内容は`Can't verify CSRF token authenticity.Completed 422 Unprocessable Entity ~~`です。
CSRF対策のトークンがないため、Railsから怒られてしまいます。

`application_controller.rb`の下記をコメントアウトするとエラーが出なくなります。
本来であれば、API認証のようなものをつけた方が良いとは思いますが、今回は割愛します。
[【Rails】RailsでAPIの簡単なトークン認証を実装する](http://qiita.com/Yarimizu14/items/c81a8cf1859f954b953e)

```diff:app/controllers/application_controller.rb
-   protect_from_forgery with: :exception
+   # protect_from_forgery with: :exception
```
これで作成されたTODOが返却されるかと思います。

```
$ curl -X POST localhost:5000/api/tasks -d 'task[name]=fugafuga'
{"task":{"id":6,"name":"fugafuga","is_done":false,"created_at":"2017-09-14T08:31:17.100Z","updated_at":"2017-09-14T08:31:17.100Z"}}
```

**2020/10/18 追記**

@ohara5555 さんにご指摘いただきました。
Rails 5.2 以降のバージョンを使用されている場合は、 application_controller に protect_from_forgery がないので、（ApplicationController::Base に移動した）
自分でこのように記載していただくか、

```diff
class Api::TasksController < ApplicationController
+  protect_from_forgery
end
```
config で設定値を上書きもできるようです。
[https://edgeguides.rubyonrails.org/configuring.html#configuring-action-controller](https://edgeguides.rubyonrails.org/configuring.html#configuring-action-controller)


# Materializeの導入

CSSは[Materialize](http://materializecss.com/)というフレームワークを使用しようと思います。
マテリアルデザインを意識したものになっており、個人的に使ってみたいと思っておりました。

Gemもすでにあるので、Rails側でインストールします。
[https://github.com/mkhairi/materialize-sass](https://github.com/mkhairi/materialize-sass)

~~ドキュメントにもありますが、Turbolinksを使用する場合は、[jquery-turbolinks](https://github.com/kossnocorp/jquery.turbolinks)を入れた方が良いそうです。~~
~~今回は画面遷移としてはないので、Turbolinksが影響することはなさそうですが、とりあえず入れておきます。~~

**2018/11/9 追記**
jquery-turbolinks は必要なさそうなので削除しました。

```diff:Gemfile
+ gem 'jquery-rails'
+ gem 'materialize-sass'
+ gem 'material_icons'
```
`bundle install`した後、下記の2ファイルに追記します。
`css`は`scss`に変更しておきます。

**2018/11/9 追記**
@ryo117n さんよりご指摘があり、 `color` の `import` 文を修正しました。

```diff:app/assets/stylesheets/application.scss
+ /* Materialize */
+ @import "materialize/components/color-variables";
+ $primary-color: color("teal", "accent-3") !default;
+ $secondary-color: color("cyan", "base") !default;
+ @import 'materialize';
+ @import 'material_icons';
```
`$primary-color`と`$secondary-color`を設定しておくことで、よしなに色を合わせてくれます。
カラースキームは[こちら](http://materializecss.com/color.html)を参照してください。

```diff:app/assets/javascripts/application.js
+ //= require jquery
+ //= require materialize
  //= require rails-ujs
  //= require turbolinks
  //= require_tree .
```
# コンポーネントを使ってヘッダーを作成
ここからVue.jsを中心に画面を作っていきます。
まずはヘッダーを作成します。

元となるビューファイルはRailsの`index.html.erb`になるので、こちらにVue.jsを載せられるようにします。

```rb:app/views/home/index.html.erb
<div id="app">
  <navbar></navbar>
</div>

<%= javascript_pack_tag 'todo' %>
```
`<navbar>`というタグがありますが、Vue.js側でこのタグとコンポーネントを紐付け、表示します。
[コンポーネント](https://jp.vuejs.org/v2/guide/components.html)
また、新しく`todo.js`というファイルを`app/javascript/packs`に作成します。
（`hello_vue`を修正したも良いのですが。。。）

```js:app/javascript/packs/todo.js
import Vue from 'vue/dist/vue.esm.js'

var app = new Vue({
  el: '#app',
});
```
ここで、`import Vue from 'vue/dist/vue.esm.js'`としています。
これは、後ほどコンポーネントを使用する際に完全ビルドする必要があるからだそうです。
（すいません、まだよく分かっていません。。。）
詳しくはこちらの方が解説記事を書いてくださっています。
[Rails5.1でVue.jsで単一ファイルコンポーネントのエラーがでる](http://qiita.com/magaya0403/items/3fbe9aa20c6a66b76662)

これで`index.html.erb`内の`<div id="app">`にマウントされます。
このまま実行しても特に何もありません。

それではコンポーネントを作成します。
`packs`の下に`components`ディレクトリを作成して、そこに`header.vue`を作成します。
コンポーネントは`.vue`で作成します。

```html:app/javascript/packs/components/header.vue
<template>
  <div>
    <ul id="dropdown" class="dropdown-content">
      <li><a href="#">Top</a></li>
      <li><a href="#">About</a></li>
      <li><a href="#">Contact</a></li>
    </ul>
    <nav>
      <div class="nav-wrapper container">
        <a href="/" class="brand-logo left">Todo Application</a>
        <ul class="right hide-on-med-and-down">
          <li><a href="#">Top</a></li>
          <li><a href="#">About</a></li>
          <li><a href="#">Contact</a></li>
        </ul>
        <ul class="right hide-on-large-only">
          <li>
            <a class="dropdown-button" href="#!" data-activates="dropdown">
              Menu<i class="material-icons right">arrow_drop_down</i>
            </a>
          </li>
        </ul>
      </div>
    </nav>
  </div>
</template>
```
これを`todo.js`に登録します。

```diff:app/javascript/packs/todo.js
  import Vue from 'vue/dist/vue.esm.js'
+ import Header from './components/header.vue'

 var app = new Vue({
   el: '#app',
+  components: {
+    'navbar': Header,
+  }
 });
```
`navbar`という名前でコンポーネントとして登録します。
（`header`だと`<header>`タグがすでにHTML5に存在しているため。）
これで`<navbar>`タグが使用できるようになりました。

サーバーを再起動してアクセスすると下図のようなヘッダーができているのではないでしょうか？

![スクリーンショット 2017-09-14 19.33.37.png](https://qiita-image-store.s3.amazonaws.com/0/118817/0aeaa113-314c-0f84-f6cd-5fcc9b0e8436.png)
# Vue-Routerを使用してSPAっぽく
Vue-Routerを使用することで、登録されたパスとコンポーネントで画面内を差し替えることができます。
[Vue-Router](https://router.vuejs.org/ja/)

`yarn`を使って`vue-router`を追加します。

```
$ yarn add vue-router
```
今回は「TODO一覧（メイン画面）」、「アバウト（おまけ）」、「コンタクト（おまけ）」を用意します。
正直、メイン画面でTODO管理はできるので、あと2つはおまけです。

まずはコンポーネントを作成します。

```html:app/javascript/packs/components/index.vue
<template>
  <div>
    <p>Index</p>
  </div>
</template>
```
```html:app/javascript/packs/components/about.vue
<template>
  <div>
    <!-- 内容はお好みで -->
    <p>This is a sample of TODO application with Vue.js and Ruby on Rails.</p>
    <p>Sample code is <a href="https://github.com/naoki85/todo_app_with_vue_and_rails" target="_blank">here.</a></p>
  </div>
</template>
```
```html:app/javascript/packs/components/contact.vue
<template>
  <div>
    <!-- 内容はお好みで -->
    <p>If you want to contact me, you send mail to below address.</p>
    <p>test@example.com</p>
  </div>
</template>
```
さて、このコンポーネントとパスを登録する`router.js`を作成します。
こちらも`router`ディレクトリを作成してそちらに作成します。

```js:app/javascript/packs/router/router.js
import Vue from 'vue/dist/vue.esm.js'
import VueRouter from 'vue-router'
import Index from '../components/index.vue'
import About from '../components/about.vue'
import Contact from '../components/contact.vue'

Vue.use(VueRouter)

export default new VueRouter({
  mode: 'history',
  routes: [
    { path: '/', component: Index },
    { path: '/about', component: About },
    { path: '/contact', component: Contact },
  ],
})
```
パスとコンポーネントを結びつけます。
また、`mode: 'history'`とすることで、HTMLのhistory APIを使用して、一見同じビュー内ですがURLを書き換えることができます。
[HTML5 Historyモード](https://router.vuejs.org/ja/essentials/history-mode.html)

また、VueRouterを使用すると、`<router-link>`と`<router-view>`というタグが使用できます。
`<router-link>`は、`<a>`タグとして変換されますが、画面遷移ではなくVueRouterに登録されたパスからコンポーネントを探します。
そして`<router-view>`の部分に表示します。

ヘッダーの各リンクを修正します。

```diff:app/javascript/packs/components/header.vue
- <li><a href="/">Top</a></li>
- <li><a href="/about">About</a></li>
- <li><a href="/contact">Contact</a></li>
+ <li><router-link to="/">Top</router-link></li>
+ <li><router-link to="/about">About</router-link></li>
+ <li><router-link to="/contact">Contact</router-link></li>
```
それぞれのコンポーネントが表示される部分を`index.html.erb`に作ります。

```diff:app/views/home/index.html.erb
 <div id="app">
   <navbar></navbar>
+  <div class="container">
+    <router-view></router-view>
+  </div>
 </div>

 <%= javascript_pack_tag 'todo' %>
```
最後に、`todo.js`に追加します。

```diff:app/javascript/packs/todo.js
  import Vue from 'vue/dist/vue.esm.js'
+ import Router from './router/router'
  import Header from './components/header.vue'
  
  var app = new Vue({
+   router: Router,
    el: '#app',
    components: {
      'navbar': Header,
    }
  });
```
これで、ヘッダーの各リンクを押すと本文が切り替わるのではないでしょうか？
URLもHistoryモードのおかげで書き換わっています。

ただ、例えば`http://localhost:5000/about`でリロードすると、Rails側でエラーになってしまいます。
たしかに`routes.rb`で登録していません。
とりあえず、`/about`でも`/contact`でも`Home#index`にとぶよう記述します。

```diff:config/routes.rb
 Rails.application.routes.draw do
   root to: 'home#index'
+  get '/about',   to: 'home#index'
+  get '/contact', to: 'home#index'
```
これで`http://localhost:5000/about`にアクセスするとわかりますが、ちゃんとURLからAboutのコンポーネントを表示してくれます。
# Axiosを使ってAPI通信
[axios](https://github.com/mzabriskie/axios)は、Ajax通信ライブラリです。
まずはこれをインストールします。

```
$ yarn add axios
```
Axiosを使用して`index.vue`にてAPI通信してタスク管理したいと思います。

まずは完成イメージを`index.vue`の`template`内に記載します。
これを書き換えていきます。

**2019/01/12 追記**
Materialize の最新バージョンを使用する場合、本記事時点の文法とは異なる場合があります。
@tktcorporation さんより、[チェックボックスの修正方法のコメント](https://qiita.com/naoki85/items/51a8b0f2cbf949d08b11#comment-e644d0df08600a5dfad2)をいただきました。
もしスタイルやJSがうまく当たっていないと感じた場合は、 [Materialize のドキュメント](https://materializecss.com/about.html)を参考にしていただきますようお願いします。

```html:app/javascript/packs/components/index.vue
<template>
  <div>
    <!-- 新規作成部分 -->
    <div class="row">
      <div class="col s10 m11">
        <input class="form-control" placeholder="Add your task!!">
      </div>
      <div class="col s2 m1">
        <div class="btn-floating waves-effect waves-light red">
          <i class="material-icons">add</i>
        </div>
      </div>
    </div>
    <!-- リスト表示部分 -->
    <div>
      <ul class="collection">
        <li id="row_task_1" class="collection-item">
          <input type="checkbox" id="task_1" />
          <label for="task_1">Sample Task</label>
        </li>
        <li id="row_task_2" class="collection-item">
          <input type="checkbox" id="task_2" />
          <label for="task_2">Sample Task</label>
        </li>
        <li id="row_task_3" class="collection-item">
          <input type="checkbox" id="task_3" />
          <label for="task_3">Sample Task</label>
        </li>
      </ul>
    </div>
    <!-- 完了済みタスク表示ボタン -->
    <div class="btn">Display finished tasks</div>
    <!-- 完了済みタスク一覧 -->
    <div id="finished-tasks" class="display_none">
      <ul class="collection">
        <li id="row_task_4" class="collection-item">
          <input type="checkbox" id="'task_4" checked="checked" />
          <label v-bind:for="task_4" class="line-through">Done Task</label>
        </li>
        <li id="row_task_5" class="collection-item">
          <input type="checkbox" id="'task_5" checked="checked" />
          <label v-bind:for="task_5" class="line-through">Done Task</label>
        </li>
      </ul>
    </div>
  </div>
</template>
```
こんな感じになります。

![スクリーンショット 2017-09-14 21.11.55.png](https://qiita-image-store.s3.amazonaws.com/0/118817/7053afd6-acd0-f2d8-450b-08971e4d9360.png)


## 一覧表示
コンポーネントの中でHTML、JS、CSSをまとめて記載することを[単一コンポーネント](https://jp.vuejs.org/v2/guide/single-file-components.html)というようです。
`.vue`ファイルの中で、そのコンポーネントで使うJSも記述することができます。
一覧をAPIで取得するために必要なものを追記します。

```diff:app/javascript/packs/components/index.vue
    <!-- 省略 -->
  </template>

+ <script>
+   import axios from 'axios';
+ 
+   export default {
+     data: function () {
+       return {
+         tasks: [],
+         newTask: ''
+       }
+     },
+     mounted: function () {
+       this.fetchTasks();
+     },
+     methods: {
+       fetchTasks: function () {
+         axios.get('/api/tasks').then((response) => {
+           for(var i = 0; i < response.data.tasks.length; i++) {
+             this.tasks.push(response.data.tasks[i]);
+           }
+         }, (error) => {
+           console.log(error);
+         });
+       },
+     }
+   }
+ </script>
```
インスタンスにプロパティとして`tasks`と`newTask`を与えます。
メソッドとして`fetchTasks`を登録し、APIで取得してきた値をループさせて`tasks`に格納します。
AxiosはJQueryと同じ感じで使えるので、使用しやすいかと思います。

[mounted](https://jp.vuejs.org/v2/api/#mounted)はVueインスタンスがマウントされたタイミングで実行されるライフサイクルフックです。
[created](https://jp.vuejs.org/v2/api/#created)もあって今回の場合、あまり違いはありませんが、[ライフサイクルダイアグラム](https://jp.vuejs.org/v2/guide/instance.html#%E3%83%A9%E3%82%A4%E3%83%95%E3%82%B5%E3%82%A4%E3%82%AF%E3%83%AB%E3%83%80%E3%82%A4%E3%82%A2%E3%82%B0%E3%83%A9%E3%83%A0)については、ちゃんと理解する必要がありそうです。

`template`内の下記の部分を書き換えます。
（未完了と完了済みを両方変えます。）
`v-for`と`v-if`を使って`tasks`プロパティの中で条件に合うものを表示しています。
[条件付きレンダリング](https://jp.vuejs.org/v2/guide/conditional.html#v-if-と-v-for)
また、[v-bind](https://jp.vuejs.org/v2/api/#v-bind)で強引に`id`名を作っています。

```diff:app/javascript/packs/components/index.vue
     <!-- リスト表示部分 -->
     <div>
       <ul class="collection">
-        <li id="row_task_1" class="collection-item">
-          <input type="checkbox" id="task_1" />
-          <label for="task_1">Sample Task</label>
-        </li>
-        <li id="row_task_2" class="collection-item">
-          <input type="checkbox" id="task_2" />
-          <label for="task_2">Sample Task</label>
-        </li>
-        <li id="row_task_3" class="collection-item">
-          <input type="checkbox" id="task_3" />
-          <label for="task_3">Sample Task</label>
-        </li>
+        <li v-for="task in tasks" v-if="!task.is_done" v-bind:id="'row_task_' + task.id" class="collection-item">
+          <input type="checkbox" v-bind:id="'task_' + task.id" />
+          <label v-bind:for="'task_' + task.id">{{ task.name }}</label>
+        </li>
       </ul>
     </div>

      <!-- 完了済みタスク一覧 -->
      <div id="finished-tasks" class="display_none">
        <ul class="collection">
-         <li id="row_task_4" class="collection-item">
-           <input type="checkbox" id="'task_4" checked="checked" />
-           <label v-bind:for="task_4" class="line-through">Done Task</label>
-         </li>
-         <li id="row_task_5" class="collection-item">
-           <input type="checkbox" id="'task_5" checked="checked" />
-           <label v-bind:for="task_5" class="line-through">Done Task</label>
-         </li>
+         <li v-for="task in tasks" v-if="task.is_done"v-bind:id="'row_task_' + task.id" class="collection-item">
+           <input type="checkbox" v-bind:id="'task_' + task.id" checked="checked" />
+           <label v-bind:for="'task_' + task.id"  class="line-through">{{ task.name }}</label>
+         </li>
        </ul>
      </div>
```
完了済みタスクを常に表示させておく必要はないと思います。
`display_none`というクラスをつけているので、ここに非表示のスタイルをあてたいと思います。
[単一コンポーネント](https://jp.vuejs.org/v2/guide/single-file-components.html)ではCSSも管理できます。

ただ、デフォルトの設定だと、コンパイル時に別でスタイルシートを出力してしまうので、これを無しにします。
[RailsとVue.js の設計覚書](http://qiita.com/midnightSuyama/items/efc5441a577f3d3abe74)
### スタイルシートをまとめて出力

```diff:config/webpack/environment.js
+ environment.loaders.get('vue').options.extractCSS = false
```

**2018/03/24 追記**  
@trandaison さんよりご指摘がありました。
webpacker 3+から、`loaders/vue.js`の中で`extractCSS = false`と修正すれば良いようです。
（devServerを見直せば良さそうですが。。）

```js:config/webpack/loaders/vue.js
//const extractCSS = !(inDevServer && (devServer && devServer.hmr)) || isProduction
const extractCSS = false
```
ただ、私が作成した時にこのファイルはなかったので、バージョンアップの際に追加されたのかもしれません。
（そこまでは追っていませんでした。。。）
`loaders/vue.js`がないバージョンをお使いの方は @kawadumax さんからもご指摘がありましたが、下記のように `extractCSS` を指定すれば良いようです。

```diff:config/webpack/environment.js
+ environment.loaders.get('vue').use[0].options.extractCSS = false
```
### スタイルの追加
`index.vue`にスタイルを追記します。
`<style>`タグ中に`scoped`という属性をつけておくと、そのファイルのみで有効なスタイルとして認識してくれます。
そのコンポーネントでしか使わないようなクラスはここで定義してしまえば良さそうです。

また、APIの返り値を使用してレンダリングする場合、API処理が終わるまでは、`{{ task.name }}`がそのまま文字列としてレンダリングされてしまいます。
（インスタンスに値がセットされたら、その値が表示されます。）
このとき、[v-cloak](https://jp.vuejs.org/v2/api/#v-cloak)というディレクティブをCSSの`display: none;`と組み合わせて使うと、インスタンスが生成されたタイミングで表示してくれます。

```diff:app/javascript/packs/components/index.vue
    // 省略
  </script>

+ <style scoped>
+   [v-cloak] {
+     display: none;
+   }
+   .display_none {
+     display:none;
+   }
+   // 打ち消し線を引く
+   .line-through {
+     text-decoration: line-through;
+   }
+ </style>
```
これで完了済みの方は非表示になったかと思います。
## ボタンを押すと、完了済みエリアが表示される
まずはVueの`methods`にボタンを押されたときのメソッドを登録します。

```diff:app/javascript/packs/components/index.vue
  methods: {
    // 省略
    },
+   displayFinishedTasks: function() {
+     document.querySelector('#finished-tasks').classList.toggle('display_none');
+   },
  }
}
```
「ボタンを押されたとき」と記載するのはかなり簡潔にかけます。
テンプレートの方で[v-on](https://jp.vuejs.org/v2/api/#v-on)を使用してクリックされたタイミングで`displayFinishedTasks`を呼んでもらいます。

```diff:app/javascript/packs/components/index.vue
    <!-- 完了済みタスク表示ボタン -->
-   <div class="btn">Display finished tasks</div>
+   <div class="btn" v-on:click="displayFinishedTasks">Display finished tasks</div>
```
## 新規作成フォームをつくる
[v-model](https://jp.vuejs.org/v2/api/#v-model)を利用することで、双方向バインディングさせることができます。
これで`<input>`タグで入力された値とインスタンスの`newTask`プロパティをバインドさせます。

```diff:app/javascript/packs/components/index.vue
- <input class="form-control" placeholder="Add your task!!">
+ <input v-model="newTask" class="form-control" placeholder="Add your task!!">
```
次に新規作成のメソッドを追加します。
APIで新規作成できた場合は、`tasks`プロパティの先頭に追加するようにします。

```diff:app/javascript/packs/components/index.vue
    methods: {
        // 省略
      },
+     createTask: function () {
+       if (!this.newTask) return;
+       
+       axios.post('/api/tasks', { task: { name: this.newTask } }).then((response) => {
+         this.tasks.unshift(response.data.task);
+         this.newTask = '';
+       }, (error) => {
+         console.log(error);
+       });
+     }
    }
```
作成ボタンを押した場合にこのメソッドを呼びたいので、[v-on](https://jp.vuejs.org/v2/api/#v-on)で登録します。

```diff:app/javascript/packs/components/index.vue
- <div class="btn-floating waves-effect waves-light red">
+ <div v-on:click="createTask" class="btn-floating waves-effect waves-light red">
    <i class="material-icons">add</i>
  </div>
```
## チェックをつけたら完了済みに移す
まずはチェックボックスにチェックがついたら更新メソッドを呼ぶようにしたいと思います。
（この辺りは毎回呼ばずに、一定時間で同期させても良いかもしれません。）

```diff:app/javascript/packs/components/index.vue
<!-- リスト表示部分 -->
<div>
  <ul class="collection">
    <li v-bind:id="'row_task_' + task.id" class="collection-item" v-for="task in tasks" v-if="!task.is_done">
-     <input type="checkbox" v-bind:id="'task_' + task.id" />
+     <input type="checkbox" v-on:change="doneTask(task.id)" v-bind:id="'task_' + task.id" />
      <label v-bind:for="'task_' + task.id" class="word-color-black">{{ task.name }}</label>
    </li>
  </ul>
</div>
```
更新用のメソッドを追加します。
更新したタイミングで、未完了部分から消し、完了済みの方に追加します。
（このあたりの処理はもっと良い書き方がある気がします。。。）

```diff:app/javascript/packs/components/index.vue
    methods: {
        // 省略
      },
+     doneTask: function (task_id) {
+       axios.put('/api/tasks/' + task_id, { task: { is_done: 1 } }).then((response) => {
+         this.moveFinishedTask(task_id);
+       }, (error) => {
+         console.log(error);
+       });
+     },
+     moveFinishedTask: function(task_id) {
+       var el = document.querySelector('#row_task_' + task_id);
+       // DOMをクローンしておく
+       var el_clone = el.cloneNode(true);
+       // 未完了の方を先に非表示にする
+       el.classList.add('display_none');
+       // もろもろスタイルなどをたして完了済みに追加
+       el_clone.getElementsByTagName('input')[0].checked = 'checked';
+       el_clone.getElementsByTagName('label')[0].classList.add('line-through');
+       el_clone.getElementsByTagName('label')[0].classList.remove('word-color-black');
+       var li = document.querySelector('#finished-tasks > ul > li:first-child');
+       document.querySelector('#finished-tasks > ul').insertBefore(el_clone, li);
+     }
    }
```
# さいごに
ここまでで、下図のような動きができるかと思います。
（いくつかスタイルはたしました。）

![sample_todo_spa_2.gif](https://qiita-image-store.s3.amazonaws.com/0/118817/ddd5650a-f745-bbd0-c98e-37da551bb2c0.gif)

ただ、テスト、および状態管理に便利なVuexは今回できなかったため、機会があればまた書きたいと思います。
もしよろしければご意見いただけると嬉しいです。

最後に、今回のソースコードはこちらになります。
[https://github.com/naoki85/todo_app_with_vue_and_rails](https://github.com/naoki85/todo_app_with_vue_and_rails)
## 2017/09/20 追記
試しにテストコードを書いてみました。
Railsの**SystemTestCase**で書いております。
理由は、Node.jsのテストフレームワークは（私にとって）学習コストが高いためです。
本当はフロントエンドと分けるのであれば、テストも分けた方が良いのではないかと思っています。
[https://github.com/naoki85/todo_app_with_vue_and_rails/commit/4a5225395fa2fe6ba9c7a3f8a421648ab28c897c](https://github.com/naoki85/todo_app_with_vue_and_rails/commit/4a5225395fa2fe6ba9c7a3f8a421648ab28c897c)

テストの中で、`within`を使用していますが、今回の場合は使用する必要はないと思います。
今後、さらに詳細なテストをする際に使用するかもと思い、残したままにしてあります。

まだ記事にできるほど試していないため、参考程度にご覧ください。
参考にさせていただいたドキュメント、記事などは下記になります。

* [A Guide to Testing Rails Applications](http://guides.rubyonrails.org/testing.html)
* [Module: Capybara::Node::Actions](http://www.rubydoc.info/gems/capybara/2.7.1/Capybara/Node/Actions)
* [Module: Capybara::Node::Matchers](http://www.rubydoc.info/gems/capybara/2.7.1/Capybara/Node/Matchers)
* [Rails 5.1のSystemTestCaseを試してみた](https://qiita.com/jnchito/items/4d01f2faa1deee36bd27)
* [使えるRSpec入門・その4「どんなブラウザ操作も自由自在！逆引きCapybara大辞典」](https://qiita.com/jnchito/items/607f956263c38a5fec24)