【Rails API + Vue.js 】はじめの一歩はやはりTODOList【Rspecもやるよ】

## はじめに
Rails API + Vue.jsでチュートリアル的にやるとしたらやっぱり最初はTODOリストでしょ〜。

ということでお品書き

- 環境構築
- Rail API出力
- Rspecテスト
- Vueへの繋ぎ込み
- Vueで軽く動く画面作成

<img width="203" alt="スクリーンショット 2019-05-19 16.39.04.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399745/e930b3d3-93ff-2c9a-3c15-62c763afb9e4.png">

## 環境構築

[devise_token_auth実装〜Rspecテストまで](https://qiita.com/disk131/items/10f33c666fcc952fcef7)　の記事で書いた環境構築と被る部分がありますが、今回はwebpack込みなのでやってみ

### 初期ファイル作成〜起動確認
vendor/bundleにbundle installしたい為、初期bundle installをスキップしてSQLはMySQLを使用するように作成しています。

```sh
$ rails new railsvuetodo --webpack=vue --skip-bundle -d mysql
```

gemfileに `, github: 'rails/webpacker'` を追記。

```gemfile
- gem 'webpacker'
+ gem 'webpacker', github: 'rails/webpacker'
```

次にbundle installします。

```sh
$ bundle install --path vendor/bundle 
```

mysql2のエラーになる場合はこちらを流してからbundle install

```sh
$ bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib"
```

bundle installが終わったら起動確認したいので、db:createしてwebpackerの設定ファイルを追加して起動してみます。

```sh
$ bundle exec rails db:create
$ bundle exec rails webpacker:install 
$ bundle exec rails s
```

これで起動できたと思います。

## 各種環境設定

generationコマンドを流したときに、Rspecファイルを生成し、余分なファイルが生成されないようにします。

```application.rb
config.generators do |g|
  g.template_engine false
  g.javascripts false
  g.stylesheets false
  g.helper false
  g.test_framework :rspec,
  fixtures: true,
  fixture_replacement: :factory_bot,
  view_specs: false,
  routing_specs: false,
  helper_specs: false,
  controller_specs: false,
  request_specs: true
end
config.api_only = true
config.middleware.use ActionDispatch::Flash
```

### rspecインストール

**gemfileに追加**

```rb
group :development, :test do
  #中略
  gem 'rspec-rails'
  gem 'factory_bot_rails'
  gem 'faker'
end
```
[factory_bot_rails](https://github.com/thoughtbot/factory_bot_rails)と[faker](https://github.com/stympy/faker)は[Rspec](https://github.com/rspec/rspec-rails)でテストするとき、テストデータを作成するのに必要なgemになります。
bundle installしときます。

```sh
$ bundle install --path vendor/bundle 
```

続いてRspecの設定ファイルをインストールします。

```sh
$ bundle exec rails g rspec:install
```

.rspecファイルを書き換えます。

```.rspec
--require rails_helper
--format documentation
```

rails_helper.rbに追記

```rails_helper.rb
#require 'rspec/rails'の下辺りに記述
Faker::Config.locale = :ja
#RSpec.configure do |config|メソッド内に記述
config.include FactoryBot::Syntax::Methods
```

これでひとまずAPI実装してRspecでテストできる環境が整いました。

## API実装

まずはモデルを作ります。

今回は、内容である`body`とタスクが終了したかどうかの値を持つ`done`をboolean型で作ります。

### モデル作成

```sh
 $ bundle exec rails g model Todo body:string done:boolean
```

このコマンドによって以下のファイルが作成されます。

```
db/migrate/20190524132256_create_todos.rb
app/models/todo.rb
spec/models/todo_spec.rb
spec/factories/todos.rb
```

マイグレーションファイルにすこし追記しますね。
`done`の`デフォルト値をfalse`とし、`null:false`にします。

```db/migrate/20190524132256_create_todos.rb
class CreateTodos < ActiveRecord::Migration[5.2]
  def change
    create_table :todos do |t|
      t.string :body
      t.boolean :done, default: false, null: false #追記箇所

      t.timestamps
    end
  end
end
```

**マイグレ！**

```sh
$ bundle exec rails db:migrate
```

モデルファイルにバリデーションを追加します。

```app/models/todo.rb
class Todo < ApplicationRecord
  validates :body, presence: true #空ダメ
end
```

### コントローラ作成

```sh
$ bundle exec rails g controller api/v1/todos
create  app/controllers/api/v1/todos_controller.rb
```

さっそく作成したコントローラファイルを修正していきます。

```app/controllers/api/v1/todos_controller.rb
class Api::V1::TodosController < ApplicationController
  before_action :set_todo, only: [:update, :destroy]

  def index
    @todo = Todo.all.order("created_at DESC")
    render json: @todo
  end

  def create
    @todo = Todo.create!(todo_params)
    #作成したデータはリストに追加していくためデータを返す
    render json: @todo
  end

  def update
    @todo.update!(todo_params)
  end

  def destroy
    @todo.destroy!
  end

  private
    def set_todo
      @todo = Todo.find(params[:id])
    end

    def todo_params
      params.require(:todo).permit(:body, :done)
    end
end
```

### ActiveModel::Serializers導入

[ActiveModel::Serializers](https://github.com/rails-api/active_model_serializers)を導入します。

```
gem "active_model_serializers"
```
`bundle install`しましょ。

シリアライザーファイル作成

```
$ bundle exec rails g serializer Todo
```

`id`と`body`と`done`だけ取得するように指定しています。

```app/serializers/todo_serializer.rb
class TodoSerializer < ActiveModel::Serializer
  attributes :id, :body, :done
end
```
難しそうなjbuilderファイルなしでAPI出力できるし、書き方もシンプルなので便利です。
ぼくは[こちら](https://sandragon.hatenablog.com/entry/2018/02/03/180018)を参考にして学習しました。


### ルートの設定

ちゃんと`api/v1`にネストしましょう。

```routes.rb
Rails.application.routes.draw do
  namespace :api, { format: "json" } do
    namespace :v1 do
      resources :todos, :only => [:index, :create, :update, :destroy]
    end
  end
end
```

API出力の実装が完了しました。

確認していきます。

### CSRF対策

CSRF対策お忘れなく！

```application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :null_session
end
```

### 外部ツールでの確認
[外部ツールでのAPI出力確認はこちらを参考に！]
(https://qiita.com/disk131/items/528bfde1456289ed9963)

createはこれでいけるでしょう。

```
{"todo":
 {"body": "タスク１"}
}
```

getで保存できてるか確認してみてください。

API出力処理おわり。

## Rspecテスト

Rspecテストをするので、**testフォルダは削除**しておきましょう。

ファクトリーズファイル、本来なら修正しますが今回はこのままでいっか！

```spec/factories/todos.rb
FactoryBot.define do
  factory :todo do
    body { "MyString" }
    done { false }
  end
end
```

### モデルテスト

今回は`validates :body, presence: true`を指定しているので、そのテストを行います。

#### テスト項目
- `body`を指定しているときレコードが作成される。
- `body`を指定していないときエラーになる。

```spec/models/todo_spec.rb
require 'rails_helper'

RSpec.describe Todo, type: :model do
  describe "validates presence" do
    context "bodyを指定しているとき" do
      let(:todo) { create(:todo) }
      it "レコードが作成される" do
        expect(todo).to be_valid
      end
    end

    context "bodyを指定していないとき" do
      let(:todo) { build(:todo, body: nil) }
      it "エラーになる" do
        todo.valid?
        expect(todo.errors.messages[:body]).to include "can't be blank"
      end
    end
  end
end
```

### コントローラのテスト

テストファイルを作成します。

```sh
$ bundle exec rails g rspec:integration api/v1/todo
```

####仕様
- indexは一覧表示。
- 今回はshow(個別ページ)はなしにしています。
- createは`bodyは入力した値`を、`doneはdefalt値(false)`でレコードを作成します。
- updateは`doneの値のみ`修正できるようにします。
- destroyは指定したレコードを削除。

**この仕様を元にテストしていきます。**

####テスト項目
- Todo一覧を取得する。
  - データを３つ作成し、3レコード取得できるか。
  - 取得したデータのkeyが`id`, `body`, `done`か。
  - レスポンスステータスが`200`であるか。
- レコードが作成される。
  - レコードを作成し、Todo.countが１レコード増えているか。
  - レスポンスステータスが`200`であるか。
- 指定したレコードが更新される。
  - デフォルトでdoneがfalseのデータが作成されるのでtrueに更新されるか。
  - done以外の値は変わりなし。
  - レスポンスステータスが`204`であるか。
- 指定したレコードが削除される。
  - レコードを作成及び削除し、Todo.countが１レコード減っているか。
  - レスポンスステータスが`204`であるか。

```spec/requests/api/v1/todos_spec.rb
require 'rails_helper'

RSpec.describe "Api::V1::Todos", type: :request do
  describe "GET /api/v1/todos" do
    subject { get(api_v1_todos_path) }
    before { create_list(:todo, 3) }
    
    it "Todo一覧を取得する" do
      subject
      res = JSON.parse(response.body)
      expect(res.count).to eq 3
      expect(res[0].keys).to eq ["id", "body", "done"]
      expect(response).to have_http_status(200)
    end
  end

  describe "POST /api/v1/todos" do
    subject { post(api_v1_todos_path, params: params) }
    let(:params) { { todo: attributes_for(:todo) } }

    it "レコードが作成される" do
      expect { subject }.to change { Todo.count }.by(1)
      expect(response).to have_http_status(200)
    end
  end

  describe "PATCH /api/v1/todos/:id" do
    subject { patch(api_v1_todo_path(todo.id), params: params) }
    let(:todo) { create(:todo) }
    let(:params) { { todo: { done: true, created_at: Time.current } } }

    it "指定したレコードが更新される" do
      expect { subject }.to change { Todo.find(todo.id).done }.from(todo.done).to(params[:todo][:done])
      expect { subject }.not_to change { Todo.find(todo.id).body }
      expect { subject }.not_to change { Todo.find(todo.id).created_at }
      expect(response).to have_http_status(204)
    end
  end

  describe "DELETE /api/v1/todos/:id" do
    subject { delete(api_v1_todo_path(todo.id)) }
    let!(:todo) { create(:todo) }

    it "指定したレコードが削除される" do
      expect { subject }.to change { Todo.count }.by(-1)
      expect(response).to have_http_status(204)
    end
  end
end
```

これでAPIの実装〜テストまでが完了しました。

```sh
$ bundle exec rspec
```
※追記
こういうエラー出るかも

```
LoadError - cannot load such file -- rexml/document
```
出たらこれgem追加

```rb
gem 'rexml'
```
bundle installしたら行けたっぽい


## Vue on rails!

webpackはもうすでに入っているので、Vueの初期ファイルを生成します。

```sh
$ bundle exec rails webpacker:install:vue
```

続いてVue出力用に`home#index`を作ります。

```sh
$ bundle exec rails g controller homes index
```

rootを`home#index`にします。

```routes.rb
Rails.application.routes.draw do
- get 'homes/index'
+ root "homes#index" 
  namespace :api, { format: "json" } do
    namespace :v1 do
      resources :todos, :only => [:index, :create, :update, :destroy]
    end
  end
end
```

次に以下のファイルを`作成`してください。

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

これで一度起動してみましょう。

```sh
$ bundle exec rails s
```
```sh
$ bundle exec bin/webpack-dev-server
```
この二つのコマンドを別々で起動してあげる必要があります。

[**foreman**](https://blog.yuhiisk.com/archive/2018/04/25/rails-and-webpacker-server.html)というgemを使うと一つのコマンドで済むので便利です。

<img width="208" alt="スクリーンショット 2019-05-25 11.48.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399745/cbf67a5f-9117-94fd-3bde-4d62fed74ad0.png">

これが表示されればオッケ！VueとRailsが繋がりました。

いよいよ画面を作っていきます。


###画面作成

`index.html.erb`で先程は`hello_vue.js`を読み込んでいたのですが`app.js`を読み込むように修正します。

```app/views/home/index.html.erb
- <%= javascript_pack_tag 'hello_vue' %>
+ <%= javascript_pack_tag 'app' %>
- <%= stylesheet_pack_tag 'hello_vue' %>
+ <%= stylesheet_pack_tag 'app' %>
```

`app.js`を作成します。
内容はhello_vue.jsと同じで、読み込み先を`home_container.vue`に変えただけです。

```app/javascript/packs/app.js
import Vue from 'vue'
import HomeContainer from '../components/home_container.vue'

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    render: h => h(HomeContainer)
  }).$mount()
  document.body.appendChild(app.$el)
})
```

[**axios**](https://github.com/axios/axios)をインストールしておきます。

`axios`はHTTP通信を簡単に行うことができるライブラリです。

```sh
$ yarn add axios
```

`home_container.vue`作成

```app/javascript/components/home_container.vue
<template>
  <div class="container">
    <h2>TODO List</h2>
    <form v-on:submit.prevent>
      <input type="text" v-model="body">
      <button @click.prevent="createTodo">保存</button>
    </form>
    <ul>
      <li v-for="(todo, index) in todos" :key="todo.id">
        <input
          type="checkbox"
          v-on:change="doneChange(index, todo.id, todo.done)"
          v-model="todo.done"
        >
        <span :class="{ done: todo.done }">{{ todo.body }}</span>
        <button v-on:click="deleteTodo(index, todo.id)">削除</button>
      </li>
    </ul>
  </div>
</template>

<script>
import axios from "axios"

export default {
  data: function () {
    return {
　　　 //todoリスト
      todos: [],
      //テキストボックスの値
      body: "",
    }
  },
  //一覧読み込み
  mounted () {
    axios.get('/api/v1/todos').then((response) => {
      this.todos = response.data
      }).catch(() => {
        alert("エラー");
      });
  },
  methods: {
    //Todo作成
    createTodo() {
      axios.post("/api/v1/todos", { body: this.body }).then((response) => {
        this.todos.unshift(response.data);
        this.body = "";
      }).catch(() => {
        alert("エラー");
      });
    },
    //checkボックスで発火する済んだかどうかのやつ
    doneChange(index, donechange_id, done_flg) {
      axios.put(`/api/v1/todos/${donechange_id}`, { done: done_flg }).then((response) => {
      }).catch(() => {
        alert("エラー");
      });
    },
    //Todo削除
    deleteTodo(index, delete_id) {
      axios.delete(`/api/v1/todos/${delete_id}`).then((response) => {
        this.todos.splice(index, 1);
      }).catch(() => {
        alert("エラー");
      });
    }
  }
}
</script>

<style lang="scss" scoped>
  ul {
    list-style: none;
    li {
      span.done {
        //チェックボックスで発火する取り消し線
        text-decoration: line-through;
      }
    }
  }
</style>
```

これで画面が作成されました。

起動して確認してみてください！

## おわり

Vue.jsってこんなもんか、見た目しょぼっ！と思われるかもしれませんが、これはほんの入り口でしかありません。

もっといっぱい機能があるし、コンポーネントのライブラリもbootstrapよりすばらしいのがたくさんあったりします。

新しいことを覚えるとき「こいつ・・・うごくぞ！」と毎度毎度感動してますよ。

さて、次はこれをTypeScriptに書き換えて今時風？に書き替えてみます。

はい、おやすも🌟