初心者向けVue.js × Railsでのアプリ作成(ToDoリスト編)

# はじめに

最近、Vue.jsとRailsでアプリを作っているのですが、Vue.jsとRailsでアプリを作る記事が少なく、勉強するのに少し不便でした。

Vue.js × Railsの記事が少ないと言っても探せばそれなりに見つかるのですが、私みたいなフロントエンドの事よくわかってない人間には、理解するのに時間がかかったりします。

ネットで記事をあさったり、そもそもJavaScriptが良くわかってないので、JavaScriptから勉強し直してみたり、Vue.js × Railsでアプリを作るだけにしては非常に遠回りしてしまいました。

## この記事について
この記事は、私みたいに**Vue.js × Railsのアプリ作成で遠回りな勉強をしている人をなくす事**を目的としています。

初心者向けにVue.js × Railsでアプリを作る記事を書いて、実装のイメージを掴んでもらえれば、私のような遠回りはなくなるはず。。。1度小さいアプリを作ってしまえば、理解度がグッと上がり、他の記事も読みやすくなるのできっと大丈夫！

また、この記事は私がRubyエンジニアなので、Rubyエンジニアから見てVue.jsをどう実装しようかという視点になってます。

## この記事を読む対象のレベル
Vue.js × Rails両方ともチュートリアルをやったくらいのレベルを対象としています。
Vue.jsもRailsもだいたいこんな感じというのがわかっていれば作れると思います。


## どんなものを作るか？ Vue.js × Railsそれぞれの役割とは？
この記事では、Vue.js × RailsでミニマムなToDoリストアプリを作っていきます。定番ですね。

私はToDoアプリを作ろうとした時、Vue.js × Railsがそれぞれどんな役割をしているのかよくわからず、実装のイメージが掴めなかったのですが、いろんな記事を読んだ結果、**RailsでAPIを作り、APIへのリクエストと返ってきたデータの表示をVue.jsで行う**というのが多かったです。今回もこの役割分担でアプリを作っていきます。

<img width="714" alt="スクリーンショット 2020-01-01 3.16.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343136/09d165d1-5529-a38c-e0bf-0b94dcd12892.png">

WEB業界での経験が浅い、もしくはこれからWEB業界を目指す方はAPIのイメージがつかみにくいかもしれませんが、一言で言うと**URLのリクエストを受けたら、URLに応じたデータを返す**ものです。

この役割のイメージが分かればRailsの部分をFirebaseに置き換えようとか、Vue.jsをReactに置き換えようとか応用が効くような気がします。

(注)私は現時点でFirebaseもReactも詳しくないので応用が効く気がするというだけ。。。詳しい方がいたら教えて下さると助かります。

## 完成後のイメージ
<img width="465" alt="スクリーンショット 2020-01-13 14.31.32.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343136/8adb64aa-ca66-3487-8e25-01c175cb6645.png">

- テキストボックスにタスクを入力して追加ボタンを押すとリストに追加されて表示される。
- チェックボックスをチェックすると取り消し線が引かれる
- 削除ボタンを押すと削除

# 実際に作ってみよう

## RailsでAPIを作る

では、実際にアプリを作ってきます。まずはRailsでAPIを作るところから。
以下のコマンドで、Railsアプリを作ります。```--webpack=vue```をするとwebpackとVue.jsのインストールが出来ます。

```
rails new todo_list --webpack=vue
```
「webpackとかまた新しい言葉出すなよ！」って方は以下のリンクを見てください！
[【５分でなんとなく理解！】Webpack入門](https://qiita.com/Shagamii/items/698a67bab0cd5eefcb4f)
Webpackとは、js、cssなどフロントで作るファイルをバンドリングしてくれるものです。

## ToDoリストを表示するHome画面を作成

ToDoを表示するHome画面を作るため、コントローラーを作成します。

```
rails g controller home
```

作成したコントローラーにindexだけ追加しておきましょう。

```/app/controllers/home_controller.rb
class HomeController < ApplicationController
  def index
  end
end
```

viewからVue.jsが呼び出せるか試しますために追加します。

```/app/views/home/index.erb
<%= javascript_pack_tag 'hello_vue' %>
```

routes.rbに以下を追加。

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

```rails s```して```localhost:3000```にアクセスします。
以下のような画面が表示されてればOKです。

<img width="545" alt="スクリーンショット 2020-01-02 23.00.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343136/45f50d47-1820-c8b9-a894-98fcec4a98bc.png">


ちなみにVue.jsを変更したら

**```bin/webpack```で更新してあげる必要があります。(重要)**


## APIの処理を作る

まずは、ToDoリストにタスクを追加するためにモデルを作っていきます。

```rb
rails generate model Task name:string is_done:boolean
```
ルーティングに以下を追加します。
表示用のhomeとデータを返すAPI用のapi::tasksを追加。

```/config/routes.rb

Rails.application.routes.draw do
  root to: 'home#index'

  namespace :api, format: 'json' do
    resources :tasks, only: [:index, :create, :destroy, :update]
  end
end
```

リクエストを受けたらデータを返すため、Tasksのコントローラーを作ります。

```
rails g controller Api::Tasks
```

マイグレーションします。

```
rails db:migrate
```

コントローラーの中身は以下のような感じ。

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

    def index
      @tasks = Task.order('created_at DESC')
    end

    def create
      @task = Task.new(task_params)

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

    def destroy
      Task.find(params[:id]).destroy!
    end

    def update
      Task.find(params[:id]).toggle!(:is_done)
    end

    private def task_params
      params.require(:task).permit(:name, :is_done)
    end
  end
end
```

APIを返す時は、htmlではなくJSONで返してあげたいので以下を追加します。
自分は実際にWEB業界に入るまでJSONに馴染みがなかったのですが、以下の形で書きます。

```/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
```


## APIの動作確認

DBにデータを入れて確認してみましょう。
コンソールを立ち上げます。

```
rails c
```
Taskモデルにデータを追加してみましょう。

```
Task.create(name: 'テスト用タスク')
```

もう一度サーバー立ち上げ

```
rails s
```

以下のアドレスで追加したデータがJSONでデータが返ればOK。

http://localhost:3000/api/tasks.json

<img width="413" alt="スクリーンショット 2020-01-02 23.40.55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343136/8c8df01d-46fe-dcda-8c3f-d8e9b6eaa974.png">



# Vue.jsでフロント作成

## コンポーネント

コンポーネントとは、名前付きの再利用可能な Vue インスタンスです。
再利用出来そうなパーツごとにコンポーネントを区切って実装するのが、どうやら重要らしい。
今回、最小限の構成でアプリを構成するためコンポーネントについては省こうか迷ったのですが、重要なので組み込みます。

わかりやすいイメージで言うと、ヘッダー、フッター、サイドナビ等は色んなページで再利用するのでコンポーネントを分けて実装するといった感じでしょうか。
今回もヘッダーとToDoリストを表示するボディ部分でコンポーネントを分けて実装したいと思います。

では、まず以下のようにToDoリスト表示部分を作って下さい。


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

<%= javascript_pack_tag 'todo' %> # todoに変更する事に注意
```

<navbar></navbar>はヘッダーのコンポーネントを表示します。
<%= javascript_pack_tag 'todo' %>でapp/javascript/packs配下のtodo.jsファイルを読み込みます。

### ヘッダーの作成
まずはヘッダー部分のコンポーネントを用意します。

```/app/javascript/packs/components/header.vue
<template>
  <h1>ToDoリスト</h1>
</template>
```

次に```/app/views/home/index.erb```から呼び出されている```app/javascript/packs/todo.js```にコンポーネントの設定をしていきます。
以下のように書くと```app/views/home/index.erb```の```<navbar></navbar>```に```/app/javascript/packs/components/header.vue```をマウントして表示してくれるようです。

```/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
  }
});
```
### Vue.jsの読み込み設定

**webpackはVue.jsの読み込み方がわからないので以下を実行します(重要)**

```/config/loaders/stylus.js
module.exports = {
  test: /\.styl$/,
  use: [
    'style-loader', 'css-loader', 'stylus-loader'
  ]
}
```

```/config/webpack/environment.js
const { environment } = require('@rails/webpacker')
const { VueLoaderPlugin } = require('vue-loader')
const vue = require('./loaders/vue')
const stylus = require('../loaders/stylus') // 作ったstylusをrequireする

environment.plugins.prepend('VueLoaderPlugin', new VueLoaderPlugin())
environment.loaders.prepend('vue', vue)
module.exports = environment
environment.loaders.prepend('stylus', stylus) // 作ったstylusをロード
```

**webpackを再読み込みしてからサーバーを再起動しましょう(重要)**

```
bin/webpack
```

```
rails s
```


rails sしてヘッダーが表示されて入ればOKです


<img width="625" alt="スクリーンショット 2020-01-13 14.10.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343136/0ba0d90a-29e6-36c1-b665-26fc8bc71396.png">

### ToDoリストを表示するボディ部分

axiosというライブラリを使って、フロントエンドからHTTPリクエストをします。
以下のコマンドでyarnでaxiosを追加して下さい。

```
yarn add axios
```

ToDoアプリのメイン部分の実装です。解説は後ほど詳しく説明します。

```/app/javascript/packs/components/index.vue
<template>
  <div>
    <div>
      <input v-model="newTask" placeholder="to doを追加して下さい">
      <div v-on:click="createTask">
        <i>追加</i>
      </div>
    </div>
    <ul>
      <li v-for="(task, index) in tasks">
        <input type="checkbox" v-model="task.is_done" v-on:click="update(task.id, index)">
        <span v-bind:class="{done: task.is_done}">{{ task.name }}</span>
        <button v-on:click="deleteTask(task.id, index)">削除</button>
      </li>
    </ul>
  </div>
</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(let i = 0; i < response.data.tasks.length; i++) {
            this.tasks.push(response.data.tasks[i]);
          }
        }, (error) => {
          console.log(error, response);
        });
      },
      createTask: function () {
        if(this.newTask == '') return;

        axios.post('/api/tasks', { task: { name: this.newTask } }).then((response) => {
          this.tasks.unshift(response.data);
          this.newTask = '';
        }, (error) => {
          console.log(error, response);
        });
      },
      deleteTask: function (task_id, index) {
        axios.delete('/api/tasks/' + task_id).then((response) => {
          this.tasks.splice(index, 1);
        }, (error) => {
          console.log(error, response);
        });
      },
      update: function (task_id) {
        axios.put('/api/tasks/' + task_id).then((response) => {
        }, (error) => {
          console.log(error);
        });
      }
    }
  }
</script>
```

作ったindex.vueを読み込んであげましょう。

```/app/javascript/packs/todo.js
import Vue from 'vue/dist/vue.esm.js'
import Header from './components/header.vue'
import Index from './components/index.vue' // 追加

var app = new Vue({
  el: '#app',
  components: {
    'navbar': Header,
    'contents' : Index // 追加
  }
});
```

ヘッダーのしたにindex.vueを表示するため、```<navbar></navbar>```の下に```<contents></contents>```を追加します。

```/app/views/home/index.erb
<div id="app">
  <navbar></navbar>
  <contents></contents>
</div>

<%= javascript_pack_tag 'todo' %>
```

チェックボックスが押されたら取り消し線を表示するためcss追加。

```app/assets/stylesheets/home.scss
#app li > span.done {
  text-decoration: line-through;
}
```


```rails s```して動くか確認してみて下さい。
実際にToDoリストを追加してみましょう。

<img width="475" alt="スクリーンショット 2020-01-13 16.31.25.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/343136/ba058699-72e4-9f48-b393-83aa7cbc9637.png">





# ToDoアプリのコード解説メモ

学習し始めだと、Vue.jsのどの行が何をやっているのかわからない事があったのでメモ付きのコードをのせます。

まずはtemplate

```erb
<template>
  <div>
    <div>
      <input v-model="newTask" placeholder="to doを追加して下さい">
      # 追加ボタンを押すとcreateTaskを実行する
      <div v-on:click="createTask">
        <i>追加</i>
      </div>
    </div>
    <ul>
      # fetchしたタスク一覧(tasks)から一つずつtaskとindexを取り出す処理
      <li v-for="(task, index) in tasks">
        # チェックボックスが押されたらv-modelのis_doneを変更して取り消し線を引く
        # updateでAPI側のデータも更新
        <input type="checkbox" v-model="task.is_done" v-on:click="update(task.id, index)">
        # タスクの表示。v-bind:classでis_doneを参照して取り消し線が引かれるかどうか判定
        <span v-bind:class="{done: task.is_done}">{{ task.name }}</span>
        # 削除ボタンを押すとdeleteTaskを実行
        <button v-on:click="deleteTask(task.id, index)">削除</button>
      </li>
    </ul>
  </div>
</template>
```
```vuejs

<script>
  import axios from 'axios';

  export default {
    data: function () {
      return {
        tasks: [],
        newTask: ''
      }
    },
    mounted: function () {
      this.fetchTasks();
    },
    methods: {
      // APIからタスク一覧を取得
      fetchTasks: function () {
        axios.get('/api/tasks').then((response) => {
          for(let i = 0; i < response.data.tasks.length; i++) {
            this.tasks.push(response.data.tasks[i]);
          }
        }, (error) => {
          console.log(error, response);
        });
      },
      // 新しいタスク作成
      createTask: function () {
        // テキストボックスが空の場合はreturnして終了
        if(this.newTask == '') return;

        // apiへ追加リクエスト
        axios.post('/api/tasks', { task: { name: this.newTask } }).then((response) => {
          // unshiftで現在のtasksの先頭にタスクを追加
          this.tasks.unshift(response.data);
          // 追加したらテキストボックスを空にする
          this.newTask = '';
        }, (error) => {
          console.log(error, response);
        });
      },
      // タスク削除
      deleteTask: function (task_id, index) {
        // apiへ削除リクエスト
        axios.delete('/api/tasks/' + task_id).then((response) => {
          this.tasks.splice(index, 1);
        }, (error) => {
          console.log(error, response);
        });
      },
      // タスク更新。今回はis_doneのみ更新だが、タスク名とか色々更新するようカスタムしても良いと思う
      update: function (task_id) {
        // apiへ更新リクエスト
        axios.put('/api/tasks/' + task_id).then((response) => {
        }, (error) => {
          console.log(error);
        });
      }
    }
  }
</script>
```

# まとめ
小さいアプリをとりあえず作ってみると理解度がかなり深まると思うので、今回のようなToDoアプリを作ってみると良いと思います。
かけ足で記事を書いてしまったのですが、これで私みたいな人間を救えるのか...???
今後も私のようにVue.js × Railsでアプリを作りたい人向けに記事を改訂して行きたいです。