【全部入り】Vue.js + RailsAPIでブログアプリを作る(vuex,vue-routerも)

近々業務で本格的にvueを使うのでアウトプットとして、一通りの機能を持ったアプリを作ってみました。

vue-cliとRailsで作成したAPIを使ってブログアプリを作っていきます。

未熟なため用語の不適切な使い方など多数散見されると思いますが、あなたの熱い想いはコメント欄で吐き出しまくってください。めっちゃありがたく頂戴いたします。

## 主な機能

- 投稿
- 編集
- 削除
- vue-routerによるページ遷移
- vuexによる状態管理
- サクセスメッセージ 、validation


## 前提
- vueコマンドが使える。
- vueをかじったことがある(v-modelとかなんとなくわかる)。

詳しい説明は[公式ドキュメント](https://jp.vuejs.org/)などを参考にしていただけると🙇‍♂️

ちなみに自分は[Vue.js入門](https://amzn.to/2K9TnJ5)という本で網羅的に勉強しました。
技術書はリーダブルコード以来だったのですが、アホみたいにわかりやすかったです。

## 環境
Mac
Rails 5.2.4.2
ruby 2.5.5
npm 6.13.7



## RailsでAPIを作る
まずはAPIから作っていきましょう。
### apiモードでRailsアプリを作成

```
$ rails new blog-app --api
```

末尾に`--api`とつけることでAPIモードでアプリを作成できます。

### コントローラーとモデルを作る
今回必要なコントローラーとモデルを作っていきましょう。

```
$ rails g controller v1::blogs
$ rails g model Blog title:string body:text
```

`v1::blogs`とすることでバージョンで名前空間を作成します。

blogsコントローラーを以下のように設定します。

```ruby:controllers/v1/blogs_controller

class V1::BlogsController < ApplicationController
  def index
    blogs = Blog.all
    render json: blogs
  end

  def update
    blog = Blog.find(params[:id])
    if blog.update(blog_params)
      render json: blog
    else
      render json: blog.errors
    end
  end

  def create
    blog = Blog.new(blog_params)
    if blog.save
      render json: blog, status: :created
    else
      render json: blog.errors, status: :unprocessable_entity
    end
  end

  def destroy
    blog = Blog.find(params[:id])
    if blog.destroy
      render json: blog
    end
  end

  private
  def blog_params
    params.require(:blog).permit(:title, :body)
  end
end
```

`indexアクション`は今回のロジック上は必要ないですが、ブラウザ上でデータを見やすくするために作成しました。

```ruby:views/blogs/index.html.erb
<% @blogs.each do |blog| %>
  <%= blog.title %>
  <%= blog.body %>
<% end %>
```

簡単にデータを表示できるように記述しておきます。


```ruby:config/routes.rb
Rails.application.routes.draw do
  namespace :v1 do
    resources :blogs, only: [:create, :destroy, :index, :update]
  end
end
```

ここもきっちり名前空間で区切っておきましょう。


### クロスドメインによるエラーを解決
基本的な機能はできたのですが、このままだとAPI通信をした際にconsoleで以下のエラーが出てしまいます。

`Access to XMLHttpRequest at 'https://... ' from origin 'http://... ' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource`

このエラーを解決するために以下の作業をします。

```ruby:gemfile
gem 'rack-cors'
```
デフォルトではコメントアウトされているので外してあげてください。

```
$ bundle install
```

`rack-cors`を入れると以下のファイルが現れるので同様に記述してください。

```ruby:config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*' #<=全てのオリジンからのリクエストを許可 

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
```

ここまででAPIの作成は終了です。いよいよvueのプロジェクトに入りましょう。

## vue-cliをインストール
まずはvue-cliをインストールして環境を作ります。以下の記事がわかりやすいです。
[Vue.js を vue-cli を使ってシンプルにはじめてみる](https://qiita.com/567000/items/dde495d6a8ad1c25fa43)

インストールできたら以下のコマンドでプロジェクトを作成します。

```
$ vue create blog-app
```

Manuallyを選択。

```
? Please pick a preset: 
   default (babel, eslint) 
❯  Manually select features 
```

vuexとvue-routerを選択。

```
Vue CLI v3.4.0
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select,
 <a> to toggle all, <i> to invert selection)
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◉ Router
❯◉ Vuex
 ◯ CSS Pre-processors
 ◉ Linter / Formatter
 ◯ Unit Testing
 ◯ E2E Testing
```

この後いくつか質問が聞かれますが、全部EnterでOKです。

ターミナルで以下のようになったら成功です。
`cd`でアプリのディレクトリに移動し、`npm run serve`コマンドを実行してください。

```
Successfully created project blog-app.
👉  Get started with the following commands:
 
 $ cd blog-app
 $ npm run serve
```

うまくいっていれば以下のようになります。
![vue-cli-top.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416999/472abb0a-7a91-4337-1ef7-dc7bbf8feec0.png)

ここまででvueアプリの雛形ができました。

## 一覧表示ページを作る
まずは保存したブログが一覧で見ることができるトップページのようなものを作っていきましょう。

今回、デザインは`vuetify`というcssフレームワークにまかせますのでダウンロードしておきます。

```
$ vue add vuetify

? Choose a preset: Default
```

現状のフォルダを確認しておきましょう。

```vue:src/App.vue
<template>
  <div id="app">
    <v-container>
      <router-view/>
    </v-container>
  </div>
</template>

<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: 50px;
}
</style>
```

デフォルトで入っている`HelloWorld`は使わないので削除しておきます。

基本的に今回作っていくページは全てこの`<router-view/>`の中に代入されていきます。

Railsアプリでいう`<%= yield %>`みたいな感じですね?

次にルーティングを設定していきます。

```js:src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Blogs from '../views/Blogs.vue' // 追加

Vue.use(VueRouter)

  const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/blogs',    // 追加
    name: 'blogs',
    component: Blogs
  }
]

const router = new VueRouter({
  routes
})

export default router
```
こうすることで`path: '/blogs'`というルートにアクセスすると、`views/Blogs.vue`ファイルが呼ばれるようになります。

この`Blogs.vue`が一覧表示ページになります。

ただ今回はルートページはvue-routerの簡単な練習もかねて`Home.vue`に設定してみました。
これはデフォルトでインストールされたフォルダです。
ちょっとだけいじっていきましょう。

```vue:src/views/Home.vue
<template>
  <div class="home">
    <h1>Welcome to Blog App !</h1>
    <router-link to="/blogs">start</router-link>
  </div>
</template>

<script>

export default {
  components: {
  }
}
</script>
```

`router-link`に`to=""`でルーティングパスを渡してあげることができます。

`start`というリンクをクリックすると先ほど設定した`/blogs`に飛ぶようになっています。

つまり`Blogs.vue`が表示されるということです。

では`Blogs.vue`を作っていきます。

```vue:src/views/Blogs.vue
<template>
  <div>
    <h1>Blogs</h1>
    <v-row>
      <v-col cols="4">
      <form>
        <v-text-field
          v-model="blog.title"
          label="Title"
        ></v-text-field>
        <v-textarea
          v-model="blog.body"
          label="Body"
        ></v-textarea>

        <v-btn class="mr-4" @click="onSubmit">Create</v-btn>
      </form>
      </v-col>
      <v-col cols="8">
        <table>
          <tr>
            <th>Title</th>
            <th>Description</th>
          </tr>
          <tr>
            <td>aa</td>
            <td>aa</td>
            <td>show</td>
            <td>Edit</td>
            <td>Destroy</td>
          </tr>
        </table>
      </v-col>
    </v-row>
  </div>
</template>

<script>
export default {
  data() {
    return {
      blog: {}
    }
  },
  methods: {
    onSubmit() {
    }
  }
}
</script> 
```
まだ何も投稿していないので適当にtableにデータを入れておきます。ここは後ほど自動化していきます。

現状だとボタンをクリックしても関数を設定していないので何もおきません。

ここまでで以下のような挙動になっていればOKです。
![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416999/0cae37d6-3329-8f07-68c6-a0591a594c32.gif)

では実際にAPI通信を挟んでデータを表示、追加してみましょう。

## APIと通信してデータを表示する
まずは`axios`という非同期でHTTP通信をしたいときに、Vue.jsでよく使われるHTTPクライアントをインストールします。
 
```
$ npm install --save axios
```

api通信のベースとなるファイルを作ります。
apiフォルダとその下にindex.jsファイルを作成してください。

```js:src/api/index.js
import axios from 'axios'

export default () => {
  return axios.create({
    baseURL: 'http://0.0.0.0:3000/v1'
  })
}
```
先ほどインストールしたaxiosをインポートして使えるようにしています。
ここで上記のようにaxiosを関数化しておくと全てのファイルで簡単に呼び出すことができます。

さらにbaseURLとしてAPI側のURLを指定しておくと、リクエストの際にいちいち`'http://0.0.0.0:3000/v1/blogs'`などのように書かなくてよくなります。
`'/blogs'`だけで上記のURLと同じ通信することができるのです。

### vuexで状態を管理できるようにする

src/store/index.jsを以下のように書き換えます。

1. 先ほど作成したapiファイルをimport
2. state,actions,mutationsを書き換える
3. modulesは今回使わないので消す

```js:src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from '@/api/index'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    blogs: []//④
  },
  mutations: {
    FETCH_BLOGS(state, blogs) {//③
      state.blogs = blogs
    }
  },
  actions: { 
    async fetchBlogs({ commit }) {//①
      await axios().get('/blogs')
        .then(res => {
          commit('FETCH_BLOGS', res.data)//②
        })
        .catch(e => console.log(e))
    }
  }
})
```
vuexは以下の公式の画像でよく説明されます。↓
![vuex.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416999/f397a645-dab4-e2c4-a0b1-20a0afc6a1a8.png)

流れを簡単に説明すると、

1. ブラウザでクリックなどのイベントが起こる
2. actionsでAPIとの通信を挟む
3. mutationsにactionsで通信したデータをコミットする
4. stateに最終状態として挿入する

みたいな感じです。
上のコードの番号①〜④の順で実行されます。

では、サーバーから取ってきた情報を実際に表示させてみましょう。

以前作ったRailsAPIアプリを起動します(いちいち起動するのがめんどくさい人はRailsアプリだけ先にデプロイしてしまってもOKです)。

```
 $ rails s -b 0.0.0.0
```

このときポート番号が両方とも同じだと衝突してエラーになってしまうので注意です。
幸い、vue-cliはデフォルトで`8080`、railsは`3000`を使っているので今回は大丈夫です。

まだサーバーにデータが何も入っていない状態なのでconsoleから作成してみましょう。

1. ターミナルで`rails c`コマンド
2. `Blog.create(title: "test1", body: "This is a test1")`で適当に何かデータを追加

```
$ rails c
Running via Spring preloader in process 2127
Loading development environment (Rails 5.2.4.2)
irb(main):001:0> Blog.create(title: "test1", body: "This is a test1")
   (0.1ms)  begin transaction
  Blog Create (8.0ms)  INSERT INTO "blogs" ("title", "body", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["title", "test1"], ["body", "This is a test1"], ["created_at", "2020-04-17 08:13:47.969926"], ["updated_at", "2020-04-17 08:13:47.969926"]]
   (3.4ms)  commit transaction
=> #<Blog id: 1, title: "test1", body: "This is a test1", created_at: "2020-04-17 08:13:47", updated_at: "2020-04-17 08:13:47">
```

これで現在サーバーにはデータが一つだけ入っている状態になります。
API通信をして`Blogs.vue`ページ上に表示させましょう。

`App.vue`に以下を追加します。

```vue:src/App.vue
<script>
export default {
  created() {
    this.$store.dispatch('fetchBlogs')
  },
}
</script>
```

ここでライフサイクルの中の`created`というフックを実行します。

> インスタンスが作成された後に同期的に呼ばれます。この段階では、インスタンスは、データ監視、算出プロパティ、メソッド、watch/event コールバックらのオプションのセットアップ処理が完了したことを意味します。

これで何かしらのアクションが起こるたびに先ほど定義した`fetchBlogs`アクションを実行します。

これでサーバーからデータを取得できるようになりました。
`Blogs.vue`に取得したデータを表示できるようにしていきましょう。

```vue:src/views/Blogs.vue
<template>
  <div>
    <h1>Blogs</h1>
    <v-row>
      <v-col cols="4">
      <form>
        <v-text-field
          v-model="blog.title"
          label="Title"
        ></v-text-field>
        <v-textarea
          v-model="blog.body"
          label="Body"
        ></v-textarea>

        <v-btn class="mr-4" @click="onSubmit">Create</v-btn>
      </form>
      </v-col>
      <v-col cols="8">
        <table>
          <tr>
            <th>Title</th>
            <th>Description</th>
          </tr>
          <tr v-for="blog in blogs" :key="blog.id">  <!-- 変更 ③-->
            <td>{{ blog.title }}</td> <!-- 変更 ④-->
            <td>{{ blog.body }}</td> <!-- 変更 ④-->
            <td>show</td>
            <td>Edit</td>
            <td>Destroy</td>
          </tr>
        </table>
      </v-col>
    </v-row>
  </div>
</template>

<script><img width="940" alt="スクリーンショット 2020-04-17 17.29.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416999/7ce646f0-1161-06fe-4200-db60b9b4991f.png">

import { mapState } from 'vuex' //変更 ①

export default {
  computed: { //変更 ②
    ...mapState(['blogs'])
  },
  data() {
    return {
      blog: {}
    }
  },
  methods: {
    onSubmit() {
    }
  }
}
</script> 
```

①〜④の順にデータが移動します。
1. vuexに簡単にアクセスできるようにする記述
2. `src/store/index.js`の中の`state/blogs`を取得し、利用できるようにする
3. `v-for="blog in blogs"`で②で取得した`blogs`のレコードを一つずつ`blog`として展開する
4. `blog.title`とすることでレコード上のtitleが一つずつ表示される



ここまでで http://0.0.0.0:3000/v1/blogs にアクセスすると以下の画像のような状態に(僕のは使いまわしているのでid: 66となってますが、id: 1があればOKです)。
<img width="940" alt="スクリーンショット 2020-04-17 17.29.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416999/e44acd52-eedc-2e3d-2402-8d61f1b7501e.png">

http://localhost:8080/#/blogs にアクセスすると以下のようになります。
<img width="946" alt="スクリーンショット 2020-04-17 17.29.26.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416999/575a1d46-1c41-7912-8e58-19d232016dd9.png">

ここまででサーバーと通信してデータを取得し、vue側で表示できるようになりました。
次はブログを投稿できるようにしていきましょう。

## ブログを投稿できるようにする
まずは投稿フォームを単一コンポーネントに切り出します。

```vue:src/views/AddBlog.vue
<template>
  <form>
    <v-text-field
      v-model="blog.title"
      label="Title"
    ></v-text-field>
    <v-textarea
      v-model="blog.body"
      label="Body"
    ></v-textarea>

    <v-btn class="mr-4" @click="onSubmit">Create</v-btn>
  </form>
</template>

<script>
export default {
  data() {
    return {
      blog: {},
    }
  },
  methods: {
    onSubmit() {
    }
  }
}
</script>
```

`Blogs.vue`にあったコードを切り取ってきます。

これをBlogs.vueにimportします。

```vue:src/views/Blogs.vue
<template>
  <div>
    <h1>Blogs</h1>
    <v-row>
      <v-col cols="4">
        <AddBlog /> <!-- 追加 -->
      </v-col>
      <v-col cols="8">
        <table>
          <tr>
            <th>Title</th>
            <th>Description</th>
          </tr>
          <tr v-for="blog in blogs" :key="blog.id">
            <td>{{ blog.title }}</td>
            <td>{{ blog.body }}</td>
            <td>show</td>
            <td>Edit</td>
            <td>Destroy</td>
          </tr>
        </table>
      </v-col>
    </v-row>
  </div>
</template>

<script>
import { mapState } from 'vuex'
import AddBlog from './AddBlog' //追加

export default {
  components: {
    AddBlog  //追加
  },
  computed: {
    ...mapState(['blogs'])
  },
  data() {
    return {
      blog: {}
    }
  },
  methods: {
    onSubmit() {
    }
  }
}
</script> 
```
コンポーネントを分けただけなので http://localhost:8080/#/blogs にアクセスしても特に変わりはないかと思います。

ではvuexを使って投稿機能を作っていきましょう。
まずは`create`ボタン`を押したらvuexの`addBlog`アクションにアクセスするように`AddBlog.vue`を書き換えていきます。

```vue:src/views/AddBlog.vue
<template>
  <form>
    <v-text-field
      v-model="blog.title"
      label="Title"
    ></v-text-field>
    <v-textarea
      v-model="blog.body"
      label="Body"
    ></v-textarea>

    <v-btn class="mr-4" @click="onSubmit">Create</v-btn>
  </form>
</template>

<script>
export default {
  data() {
    return {
      blog: {},
    }
  },
  methods: {
    async onSubmit() {
      await this.$store.dispatch('addBlog', this.blog) //追加
    }
  }
}
</script>
```

`this.$store.dispatch`で第一引数(addBlog)に指定したvuexのアクションに第二引数(this.blog)の情報を渡します。
ここの`this.blog`はdataの中に入っているtitleとbodyです。

ではvuexにaddBlogアクションを作っていきます。

```js:src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from '@/api/index'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    blogs: []
  },
  mutations: {
    FETCH_BLOGS(state, blogs) {
      state.blogs = blogs
    },
    ADD_BLOG(state, blog) { // 追加 ③第一引数はstate、第二引数は②でコミットされたsavedBlogが渡ってくる
      const blogs = state.blogs.concat(blog)
      state.blogs = blogs
    }
  },
  actions: {
    async fetchBlogs({ commit }) {
      await axios().get('/blogs')
        .then(res => {
          commit('FETCH_BLOGS', res.data)
        })
        .catch(e => console.log(e))
    },
    async addBlog({ commit }, blog) { // 追加 ① 、第二引数blogに先ほどのthis.blogが入る
      const res = await axios().post('/blogs', blog)
      const savedBlog = res.data
      commit('ADD_BLOG', savedBlog) // ②
      return savedBlog
    },
  }
})
```

1. `AddBlog.vue`からアクセスされた`actions addBlog`にデータを送信
2. `ADD_BLOG mutations`にコミット
3. `state`の`blogs`に渡されたデータを入れる


ここまでで以下のようになればOKです。
![ezgif.com-video-to-gif (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416999/940866b1-dbe8-0b16-cc00-64531b21e98f.gif)

投稿したあともフォームにデータが残るのは嫌なので、以下のように追記します。

```js:/src/views/AddBlog.vue

<script>
export default {
  data() {
    return {
      blog: {},
    }
  },
  methods: {
    async onSubmit() {
      await this.$store.dispatch('addBlog', this.blog)
      this.blog.title = '' //追記
      this.blog.body = '' //追記
    }
  }
}
</script>
```

これで投稿したらフォームの中身がリセットされるようになりました。

## 投稿時に投稿詳細ページに遷移する
現在までのアプリだと投稿したらその場で`Blogs.vue`にデータが追加されてテーブルのレコードが増えていくという感じです。
正直、他のvueのサンプルアプリはこんなのばっかりだったので、今回は投稿時に個別の詳細ページに遷移させてみたいと思います。

以下の流れで作成します。
1. ルーティング作成
2. AddBlog.vueに投稿時に遷移するロジックを追記
3. 投稿詳細ページを作成

ではルーティングから書いていきます。

```js:store/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Blogs from '../views/Blogs.vue' 
import Blog from '../views/Blog.vue' // 追加

Vue.use(VueRouter)

  const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/blogs',    
    name: 'blogs',
    component: Blogs
  },
  {
    path: '/blogs/:id',  // 追加
    name: 'show-blog',
    component: Blog,
    params: true
  }
  
]

const router = new VueRouter({
  routes
})

export default router
```
`params: true`とすることで投稿時に取得したidを埋め込み動的なルーティングになります。

次に`AddBlog.vue`に追記していきます。

```js:src/views/AddBlog.vue
<script>
export default {
  data() {
    return {
      blog: {},
    }
  },
  methods: {
    async onSubmit() {
      const blog = await this.$store.dispatch('addBlog', this.blog) // 変更
      this.$router.push({ name: 'show-blog', params: { id: blog.id }}) // 追記
    }
  }
}
</script>
```
`this.$router.push`とすることで指定したルーティング先のページに飛びます。

これはちょっとややこしいですが、以下のように`return`することで作られたばかりのデータの情報を返してくれているので実行できています。

```js/src/store/index.js

async addBlog({ commit }, blog) {
      const res = await axios().post('/blogs', blog)
      const savedBlog = res.data
      commit('ADD_BLOG', savedBlog)
      return savedBlog // この部分でpostされたデータを返している
}
```


では実際に詳細ページである`Blog.vue`を作成します。

```vue:src/views/Blog.vue
<template>
  <div>
    <p>Title: {{ blog.title }}</p>  <!-- ④ -->
    <p>Body: {{ blog.body }}</p>
    <router-link to="/blogs">Back</router-link>
  </div>
</template>

<script>
import { mapState } from 'vuex' // ①

export default {
  computed: {
    ...mapState(['blogs']), // ②
    blog() { // ③
      return this.blogs.find(blogId => blogId.id === this.$route.params.id) || {}  
    }
  }
}
</script>
```
1. storeの情報を簡潔にかけるように`mapState`をimport
2. state/blogsを展開(this.blogsとして使える)
3. `path: '/blogs/:id'`にマッチする個別情報をstate/blogsの中から見つけて`blog()`として関数化する
4. ③のblogを展開し、個別のtitle,bodyを表示


ここまでで投稿したら詳細ページに遷移するようになりました。
![ezgif.com-video-to-gif (2).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416999/31a0d948-e0f3-78d8-f22c-c27f13725af8.gif)

次は編集機能、削除機能をつけていきます。

## 編集機能を作る
投稿を編集できるようにします。

1. 編集用のルーティングを作成
2. 編集ページを作成
3. vuex内で編集のロジックを追記

まずはルーティングから書いていきましょう。

```js:src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import Blogs from '../views/Blogs.vue'
import Blog from '../views/Blog.vue'
import EditBlog from '../views/EditBlog.vue' // 追加

Vue.use(VueRouter)

  const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/blogs',
    name: 'blogs',
    component: Blogs
  },
  {
    path: '/blogs/:id',
    name: 'show-blog',
    component: Blog,
    params: true
  },
  {
    path: '/blogs/:id/edit', // 追加
    name: 'edit-blog',
    component: EditBlog,
    params: true
  }
]

const router = new VueRouter({
  routes
})

export default router
```

詳細ページの時とほとんど同じですね。

次に`Blog.vue`と`Blogs.vue`に`editボタン`を書いていきます。

```vue:src/views/Blog.vue
<template>
  <div>
    <Flash />
    <p>Title: {{ blog.title }}</p>
    <p>Body: {{ blog.body }}</p> <!-- 追加↓ -->
    <router-link :to="{ name: 'edit-blog', params: { id: blog.id }}">Edit</router-link> | <router-link to="/blogs">Back</router-link>
  </div>
</template>
```

```vue/src/views/Blogs.vue
<template>
  <div>
    <Flash />
    <h1>Blogs</h1>
    <v-row>
      <v-col cols="4">
        <AddBlog />
      </v-col>
      <v-col cols="8">
        <table>
          <tr>
            <th>Title</th>
            <th>Description</th>
          </tr>
          <tr v-for="blog in blogs" :key="blog.id">
            <td>{{ blog.title }}</td>
            <td>{{ blog.body }}</td>　<!-- 追加↓ -->
            <td><router-link :to="{ name: 'show-blog', params: { id: blog.id }}">show</router-link></td>
            <td><router-link :to="{ name: 'edit-blog', params: { id: blog.id }}">edit</router-link></td>
            <td>delete</td>
          </tr>
        </table>
      </v-col>
    </v-row>
  </div>
</template>
```

ついでに詳細ページへのリンクも加えておきましょう。

続いて編集ページを作っていきます。

```vue:src/views/EditBlog.vue
<template>
  <div>
    <h2>Editing Blog</h2>
    <form>
      <v-text-field
        v-model="blog.title"
        label="Title"
      ></v-text-field>
      <v-textarea
        v-model="blog.body"
        label="Body"
      ></v-textarea>

      <v-btn class="mr-4" @click="updateBlog">Update</v-btn>
    </form>
  </div>
</template>

<script>
import { mapState } from 'vuex'

export default {
  computed: {
    ...mapState(['blogs']),
    blog() {
      return this.blogs.find(b => b.id == this.$route.params.id) || {}
    }
  },
  methods: {
    async updateBlog() {
      const blog = await this.$store.dispatch('editBlog', this.blog) // ①
      this.$router.push({ name: 'show-blog', params: { id: blog.id }})
    }
  }
}
</script>
```

この時点で気付いたのですが、formの部分とかは共通なのでコンポーネント分けした方が良いです笑

今回はこのままいかせてください。

ここでは①の部分でstoreの中の`editBlog`アクションに編集した内容で`this.blog`を送ってます。

ではその`editBlog`アクションを`store/index.js`の中に作っていきましょう。

```js:/src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from '@/api/index'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    blogs: []
  },
  mutations: {
    FETCH_BLOGS(state, blogs) {
      state.blogs = blogs
    },
    ADD_BLOG(state, blog) {
      const blogs = state.blogs.concat(blog)
      state.blogs = blogs
    },
    EDIT_BLOG(state, blog) {  // 追記
      state.blogs.forEach(b => {
        if (b.id === blog.id) {
          b = blog
        }
      })
    }
  },
  actions: {
    async fetchBlogs({ commit }) {
      await axios().get('/blogs')
        .then(res => {
          commit('FETCH_BLOGS', res.data)
        })
        .catch(e => console.log(e))
    },
    async addBlog({ commit }, blog) {
      const res = await axios().post('/blogs', blog)
      const savedBlog = res.data
      commit('ADD_BLOG', savedBlog)
      return savedBlog
    },
    async editBlog({ commit }, blog) { // 追記
      const res = await axios().put(`/blogs/${blog.id}`, blog)
      const editedBlog = res.data
      commit('EDIT_BLOG', editedBlog)
      return editedBlog
    }
  }
})
```

`axios().put`とすることでサーバーに対して更新の処理を行うリクエストを送ります。

そして`mutasions`では`state.blogs`の中で、送られてきたIDと合致するものの`blog`を書き換えるロジックを書いています。

詳細ページにも`editボタン`を追加しておきましょう。

```vue:src/views/Blog.vue
<template>
  <div>
    <p>Title: {{ blog.title }}</p>
    <p>Body: {{ blog.body }}</p>
    <router-link :to="{ name: 'edit-blog', params: { id: blog.id }}">edit</router-link> | <router-link to="/blogs">Back</router-link> <!-- 追加 -->
  </div>
</template>
```


ここまでで編集機能ができました。
![ezgif.com-video-to-gif (3).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416999/38935763-f6c5-f179-87ee-9cd9d98a3003.gif)

次に削除機能を作っていきましょう。

## 投稿削除機能を作る
投稿したブログを削除する機能を作っていきます。

1. 削除ボタンを作る
2. vuexに削除のロジックをかく

削除ボタンは`Blogs.vue`に書くことにします。

```vue:src/views/Blogs.vue
<template>
  <div>
    <h1>Blogs</h1>
    <v-row>
      <v-col cols="4">
        <AddBlog />
      </v-col>
      <v-col cols="8">
        <table>
          <tr>
            <th>Title</th>
            <th>Description</th>
          </tr>
          <tr v-for="blog in blogs" :key="blog.id">
            <td>{{ blog.title }}</td>
            <td>{{ blog.body }}</td>
            <td><router-link class="button_link" :to="{ name: 'show-blog', params: { id: blog.id }}">[ show ]</router-link></td>
            <td><router-link class="button_link" :to="{ name: 'edit-blog', params: { id: blog.id }}">[ edit ]</router-link></td>
            <td><span class="button_link" @click="deleteBlog(blog)">[ delete ]</span></td> <!-- 追加 -->
          </tr>
        </table>
      </v-col>
    </v-row>
  </div>
</template>

<script>
import { mapState } from 'vuex'
import AddBlog from './AddBlog'

export default {
  components: {
    AddBlog
  },
  computed: {
    ...mapState(['blogs'])
  },
  data() {
    return {
      blog: {}
    }
  },
  methods: {
    deleteBlog(blog) { // 追加
      this.$store.dispatch('deleteBlog', blog)
    }
  }
}
</script> 

<style scoped>
.button_link {
  cursor: pointer;
  color: blue;
  text-decoration: underline;
}
</style>
```

今回はアクションに引数`blog`を渡してあげています。
`v-for`で回したうちの一つの情報を持っています。

これを`this.$store.dispatch('deleteBlog', blog)`の状態でvuexにアクセスして渡しています。

```js:src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from '@/api/index'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    blogs: [],
    flash_message: {
      status: false,
      message: ''
    }
  },
  mutations: {
    setMessage(state, payload) {
      state.flash_message = payload
    },
    FETCH_BLOGS(state, blogs) {
      state.blogs = blogs
    },
    ADD_BLOG(state, blog) {
      const blogs = state.blogs.concat(blog)
      state.blogs = blogs
    },
    EDIT_BLOG(state, blog) {
      state.blogs.forEach(b => {
        if (b.id === blog.id) {
          b = blog
        }
      })
    },
    DELETE_BLOG(state, blogId) { // 追加 ②
      const blogs = state.blogs.filter(b => b.id != blogId)
      state.blogs = blogs
    }
  },
  actions: {
    async fetchBlogs({ commit }) {
      await axios().get('/blogs')
        .then(res => {
          commit('FETCH_BLOGS', res.data)
        })
        .catch(e => console.log(e))
    },
    async addBlog({ commit }, blog) {
      const res = await axios().post('/blogs', blog)
      const savedBlog = res.data
      commit('ADD_BLOG', savedBlog)
      return savedBlog
    },
    async editBlog({ commit }, blog) {
      const res = await axios().put(`/blogs/${blog.id}`, blog)
      const editedBlog = res.data
      commit('EDIT_BLOG', editedBlog)
      return editedBlog
    },
    async deleteBlog({ commit }, blog) { // 追加 ①
      await axios().delete(`/blogs/${blog.id}`, blog)
      commit('DELETE_BLOG', blog.id)
    }
  }
})
```

`axios().delete`で削除用のリクエストを飛ばします。

②では`state.blogs.filter(b => b.id != blogId)`でstate.blogs`の中から、送られてきたid以外のもので新しい配列を作って返しています。

送っているidは削除ボタンを押した要素のidなので、それ以外で配列を作っているということです。

ここまでで削除機能ができました。


基本的な機能はこれで完成です。
あとはvalidationとアクション成功時のflashメッセージを作っていきます。

## validationを作成する
新規投稿、編集時にフォームが空だった場合、送信できないようにします。

```vue:src/views/AddBlog.vue
<template>
  <v-form ref="checkForm">
    <v-text-field
      v-model="blog.title"
      label="Title"
      :rules="[required('Title')]"
    ></v-text-field>
    <v-textarea
      v-model="blog.body"
      label="Body"
      :rules="[required('Body')]"
    ></v-textarea>

    <v-btn class="mr-4" @click="onSubmit">Create</v-btn>
  </v-form>
</template>

<script>
export default {
  data() {
    return {
      blog: {},
      required(propertyType) { 
        return v => v && v.length > 0 || `You must input a ${propertyType}`
      }
    }
  },
  methods: {
    async onSubmit() {
      if (this.$refs.checkForm.validate()) {
        const blog = await this.$store.dispatch('addBlog', this.blog)
        this.$router.push({ name: 'show-blog', params: { id: blog.id }})
      }
    }
  }
}
</script>
```

これはcssフレークーワーク`vuetify`の記述方法を使っています。
`EditBlog.vue`も全く同じように記述します。

これでvalidationは完成です。
![ezgif.com-video-to-gif (4).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416999/ec4fe6ad-1971-870a-d0ab-30f8e1d8ef97.gif)


最後に投稿や編集、削除が成功した際にサクセスメッセージを出すようにしていきましょう。

## Flashメッセージを作成する

1. vuexでメッセージの状態を管理できるようにする
2. Flash.vueを作成
3. Flash.vueをメッセージを表示させたいページにimport(今回はBlog.vue、Blogs.vue)


```js:src/store/index.js
state: {
    blogs: [],
    flash_message: { // 追加
      status: false,
      message: ''
    }
  },
  mutations: {
    setMessage(state, payload) { // 追加
      state.flash_message = payload
    },
```

```vue:src/components/Flash.vue
<template>
  <transition name="fade">
    <div v-if="flash.status" class="success mb-3">{{ flash.message }}</div>
  </transition>
</template>

<script>
export default {
  computed: {
    flash() {
      return this.$store.state.flash_message
    }
  }
}
</script>

<style scoped>
.success {
  color: green;
}
.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.5s;
}
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
  opacity: 0;
}
</style>
```

まずは投稿時にサクセスメッセージを`true`にし、メッセージをvuexにコミットしておきます。

```js:src/views/AddBlog.vue
methods: {
    async onSubmit() {
      if (this.$refs.checkForm.validate()) {
        const blog = await this.$store.dispatch('addBlog', this.blog)
        this.$store.commit('setMessage', {
          status: true,
          message: 'Blog was successfully created.'
        })
        setTimeout(() => {
          this.$store.commit('setMessage', {})
        }, 2000)
        this.$router.push({ name: 'show-blog', params: { id: blog.id }})
      }
    }
  }
```

次に`AddBlog.vue`から投稿した際に`Blog.vue`でサクセスメッセージを表示できるようにします。

```vue:src/views/Blog.vue
<template>
  <div>
    <Flash /> // 追加
    <p>Title: {{ blog.title }}</p>
    <p>Body: {{ blog.body }}</p>
    <router-link :to="{ name: 'edit-blog', params: { id: blog.id }}">edit</router-link> | <router-link to="/blogs">Back</router-link>
  </div>
</template>

<script>
import { mapState } from 'vuex'
import Flash from '@/components/Flash.vue' // 追加

export default {
  components: { // 追加
    Flash
  },
  computed: {
    ...mapState(['blogs']),
    blog() {
      return this.blogs.find(blogId => blogId.id === this.$route.params.id) || {}
    }
  }
}
</script>
```

`Blogs.vue`にも記述しておきましょう。

```vue:src/views/Blogs.vue
<template>
  <div>
    <Flash /> // 追加
    <h1>Blogs</h1>
    <v-row>
      <v-col cols="4">
        <AddBlog />
      </v-col>
      <v-col cols="8">
        <table>
          <tr>
            <th>Title</th>
            <th>Description</th>
          </tr>
          <tr v-for="blog in blogs" :key="blog.id">
            <td>{{ blog.title }}</td>
            <td>{{ blog.body }}</td>
            <td><router-link class="button_link" :to="{ name: 'show-blog', params: { id: blog.id }}">[ show ]</router-link></td>
            <td><router-link class="button_link" :to="{ name: 'edit-blog', params: { id: blog.id }}">[ edit ]</router-link></td>
            <td><span class="button_link" @click="deleteBlog(blog)">[ delete ]</span></td>
          </tr>
        </table>
      </v-col>
    </v-row>
  </div>
</template>

<script>
import { mapState } from 'vuex'
import AddBlog from './AddBlog'
import Flash from '@/components/Flash.vue' // 追加

export default {
  components: {
    AddBlog,
    Flash // 追加
  },
  computed: {
    ...mapState(['blogs'])
  },
  data() {
    return {
      blog: {}
    }
  },
  methods: {
    deleteBlog(blog) { // 追加
      this.$store.dispatch('deleteBlog', blog)
      this.$store.commit('setMessage', {
        status: true,
        message: 'Blog was successfully destroyed.'
      })
      setTimeout(() => {
        this.$store.commit('setMessage', {})
      }, 2000)
    }
  }
}
</script> 

<style scoped>
.button_link {
  cursor: pointer;
  color: blue;
  text-decoration: underline;
}
</style>
```

![ezgif.com-video-to-gif (5).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/416999/56818a3c-4307-5be2-bb3b-426c783719d6.gif)


以上で完成です。デプロイする気力はありませんでした。

お付き合いいただきありがとうございます、お疲れ様でした！！

### 今回のコード
Railsの方 → https://github.com/haga0531/BlogAppApi
Vueの方 → https://github.com/haga0531/blog-app-vue
(なんかアプリ作りながら記事書いていたらcommit数一回というゴミみたいな危機管理能力になっていたのは許してください)


## まとめ
どの記事もtodoアプリばっかりだったので少しだけレベルアップしたアプリを作ってvuex、vue-routerの練習をしてみました。

コンポーネントの分け方は甘々だし、用語の使い方がゴチャゴチャなことも多くあると思います。
ドMなので厳し目にコメントくださると画面の前で土下座しながら修正させていただくので、ぜひご指摘のほどよろしくお願いします。

いや〜、しかし書いてて超楽しいですね、vue。

今後は業務でも使用している`Nuxt.js`や`Firebase`、`Java`などにも取り組んでいきます。
次は認証ありのアプリを作ってみます。

## 参考にさせていただいた神記事たち

### [[入門]Rails API × Nuxt SPA × Firebase Authで作る Todo Appチュートリアル](https://qiita.com/saongtx7/items/d97ef5aec393e704fd3f)
インターン先の超尊敬している先輩の記事です。
この記事をみながらQiita独特の書き方的なのを学びました。」ちなみに上記チュートリアルは僕自身3周しました。神でした。僕の記事読むよりこっち読んでください。

### [Vue & Vuetifyでバリデーション付きのフォームを作ってみる](https://qiita.com/tekunikaruza/items/0a68d86084d961d632ac)
vuetifyを使ったvalidationの書き方を参考にさせていただきました。