【Rails6 + Vue.js】ブックマークを管理する簡単なSPAを作ってみた話（第二章：ブックマークのCRUD実装）

前回の記事の続きになります。
前回の記事はこちら↓

https://qiita.com/showy/items/e0ed4b11cca5eb6f9944


## 本記事の概要
### 完成版イメージ
今回実装する機能（CRUD）の完成イメージです。

[![Image from Gyazo](https://i.gyazo.com/093a94f230f534244e2f6117b97b6ca9.gif)](https://gyazo.com/093a94f230f534244e2f6117b97b6ca9)

### 今回実装する機能について

####【ブックマーク新規投稿】

- モーダルを開いてフォームを表示させます。
- それぞれタイトル、URL、カテゴリーを入力し、「ADD BOOKMARK」ボタンを押すと投稿が完了し、「CANCEL」ボタンを押すとキャンセルされモーダルを閉じます。
- カテゴリーは既存のものを参照して選択できるようにします。


####【ブックマーク編集・更新】

- モーダルを開いてフォームを表示させます。
- フォームには編集するブックマークの内容をあらかじめ表示させます。
- 「UPDATE」ボタンを押すと編集内容の更新が完了します。「CANCEL」を押すと更新せずにモーダルを閉じます。

#### 【ブックマーク削除】

- 削除前に確認するメッセージをモーダルで表示します。
- モーダルの「DELETE」を押すとブックマークが削除されます。削除しない場合はモーダルの外の暗い部分をクリックすることでそのままモーダルを閉じます。

#### 【並び替え機能】

- Vue.Draggable を導入して実装します。

### 記事構成

本アプリケーションについての記事は大きく4部構成で作成しております。

- [第一章：アプリケーション作成〜ダミーデータ表示](https://qiita.com/showy/items/e0ed4b11cca5eb6f9944)
- [第二章：本記事（ブックマークのCRUD実装）]()
- [第三章：フリーワード検索、カテゴリー別絞り込み機能の実装](https://qiita.com/showy/items/7ca2e46e09371c489204)
- [第四章：ローディング表示、ページネーション、ユーザー管理機能の実装](https://qiita.com/showy/items/38747e6cebfd7f89cff3)

### 本記事の参考URL

本記事の作成にあたって以下の記事を参考にさせて頂きました。

- [[Rails+Vue.js]に係るCRUD入門〜Part４: Create, Update, Delete編〜](https://qiita.com/Moo_Moo_Farm/items/8cf46e228d59417550b1)


## 実装手順（CRUD）
それでは実装に入ります。

### ルーティングの編集
各機能の実装に入る前に、`config/routes.rb`ファイルを編集します。

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

  namespace :api, format: 'json' do
    # 編集前
    # resources :bookmarks, only: [:index]

    # 編集後
    resources :bookmarks, only: [:index, :create, :update, :destroy]
  end
end
```

### CSRF対策の無効化
CSRF（Cross-Site Request Forgery）とは、悪意のあるユーザーが不正なスクリプト等を含んだURLを用意し、本物のユーザーを装ってそのURLにアクセスさせる攻撃手法の事です。

Railsでは、erbでPOSTリクエスト等を送信する際にCSRFトークンというものを自動的に発行してCSRFを対策してくれます。
公式：[Rails セキュリティガイド -Railsガイド](https://railsguides.jp/security.html#csrf%E3%81%B8%E3%81%AE%E5%AF%BE%E5%BF%9C%E7%AD%96)

しかし、axiosを使ってVue.js側からPOSTリクエストを送信するとエラーが発生してしまうため、一度Rails側でこのCSRF対策を無効化し、後に以下の記事を参考にさせて頂きつつフロント側でCSRF対策処理を施します。

https://qiita.com/ngron/items/2faae8068baa093d6aba

それでは`bookmarks_controller`でCSRF対策の無効化処理を記述します。

```app/controllers/api/bookmarks_controller.rb
class Api::BookmarksController < ApplicationController
  protect_from_forgery :except => [:create, :update, :destroy] # <- この部分を追加
# protect_from_forgery with: :exception  <-アクションを指定しない場合
end
```

### ブックマークの新規投稿機能
まずサーバーサイドの実装をします。

#### bookmarks_controllerの編集
`app/controllers/api/bookmarks_controller.rb`に create アクションを追加します。

```app/controllers/api/bookmarks_controller.rb
class Api::BookmarksController < ApplicationController
  protect_from_forgery :except => [:create, :update, :destroy]

  def create
    @bookmark = Bookmark.new(bookmark_params)
    if @bookmark.save
      head :no_content
    else
      render json: @bookmark.errors, status: :unprocessable_entity
    end
  end

 
  private
    def bookmark_params
      params.require(:bookmark).permit(:title, :url, :category)
    end
end
```

Rails側の実装が終わったら、次はフロントを実装していきます。

#### app.vueの編集

`app/javascript/app.vue`でモーダルやフォームの部分のテンプレートと入力したデータをサーバーサイドに送信する処理を記述していきます。

全てのコードを書くと長くなってしまうので、テンプレートの主要な部分と script 部分を分けて載せます。

【テンプレート部分】

```app/javascript/app.vue
<template>
  <v-app id="app">

   <!------- 中略 -------->

   <!-- モーダルを開くためのボタン部分 -->
    <v-btn @click="togglePostModal()" style="margin: 20px 0 40px 0;">
       Bookmarkを追加する
    </v-btn>

   <!------- 中略 -------->

   <!-- 新規投稿用モーダルウィンドウ -->
      <v-dialog v-model="dialogPostFlag" width="500px" persistent>
        <v-card>
          <v-card-title class="headline blue-grey darken-3 white--text" primary-title>
            ブックマーク新規投稿
          </v-card-title>

         <!-- タイトルの入力フォーム -->
          <v-text-field v-model="postTitle" :counter="50" label="Title" required style='margin:20px;'></v-text-field> 

          <!-- URLの入力フォーム -->
          <v-text-field v-model="postUrl" label="URL" required style='margin:20px;'></v-text-field> 

          <!-- カテゴリーの入力フォーム -->
          <v-text-field v-model="postCategory" :counter="50" label="Category" required style='margin:20px;'></v-text-field>

          <!-- カテゴリーを選択できるプルダウン (categoriesForEditの配列の中身を表示) -->
          <v-select v-model='postCategory' :items="categoriesForEdit" label="Category [select]" style='margin:20px;'></v-select>

          <v-divider></v-divider>
          <v-card-actions>

            <!-- モーダルを閉じる キャンセルボタン -->
            <v-btn dark @click="togglePostModal">
              Cancel
            </v-btn>

            <v-spacer></v-spacer>
      
            <!-- postBookmark を呼んで値を送信 (submitボタン) -->
            <v-btn @click="postBookmark">
              Add Bookmark
            </v-btn>
          </v-card-actions>
        </v-card>
      </v-dialog>

  </v-app> 
</template>
```

【Vue.js部分】


```app/javascript/app.vue
<script>
import axios from 'axios';

export default {
  data: function () {
    return {
      bookmarkList: ['',''],

      // 以下を追加
      allData: ['',''],       // bookmarkの値を全て代入する空配列をもう一つ用意
      categories: ['All'],    // カテゴリーを代入する配列 (検索機能で使用)
      categoriesForEdit: [],  // カテゴリーを代入する配列 (新規投稿や編集フォームで使用)
      category: 'ALL',        // bookmarkのカテゴリー 初期値は'ALL'

      dialogPostFlag: false,  // モーダル表示：初期値をfalseに設定
      postTitle: "",          // フォームの中身の初期値は空
      postUrl: "",            // フォームの中身の初期値は空
      postCategory: "",       // フォームの中身の初期値は空
    }
  },
  mounted () {
    this.setBookmark();
  },
  methods: {
    setBookmark: function () {
      axios.get('/api/bookmarks')
      .then(response => {
        // allData の中にもデータを全て代入する
        this.allData = response.data
        this.bookmarkList = this.allData
        }
      );
      this.listCategories();
    },

    // bookmarkのカテゴリーをまとめて管理するメソッド
    listCategories: function() {
      this.categories = []         // 検索で表示するカテゴリーの選択メニュー
      this.categoriesForEdit = []  // 新規投稿や編集のフォームに表示する選択メニュー
      this.categories.push('ALL')  // `ALL`を配列に追加してメニューの一番上に表示させる

      // 配列の要素の数だけ反復処理
      for (i=0; i<this.allData.length; i++) {
        // 保存されたカテゴリーの文字列が、i 番目の　allData　のカテゴリーの何文字目で一致するかを返し、
        // 一致しない場合は -1 を返す
        // そして結果が -1 で true である場合に処理を行う
        if (this.categories.indexOf(this.allData[i].category) == -1) {
          // つまり新しいカテゴリーが保存された時それぞれの配列の中に値を push する
          this.categories.push(this.allData[i].category)
          this.categoriesForEdit.push(this.allData[i].category)
        }
      }
    },

    // モーダルを開閉するメソッド
    togglePostModal: function() {
      // モーダルの開閉状態を操作する。(ONとOFFを切り替える)
      this.dialogPostFlag = !this.dialogPostFlag
    },

    // 投稿内容を送信するメソッド
    postBookmark: function() {
      // axiosを使って /api/bookmarks コントローラーの create アクションにデータを送信
      // 第2引数でカラムにそれぞれフォームで受け取ったデータを渡す
      axios.post('/api/bookmarks', {title:this.postTitle,url:this.postUrl,category:this.postCategory})
        .then(response => {
          this.setBookmark();      // setBookmark()を呼び出す
          this.postTitle = ''      // postTitleの中身を空の状態に戻す
          this.postUrl = ''        // postUrlの中身を空の状態に戻す
          this.postCategory = ''   // postCategoryの中身を空の状態に戻す
        }
      );
      this.dialogPostFlag = !this.dialogPostFlag  // モーダルを閉じる
    },
  }
}
</script>
```

以上が新規投稿機能の実装部分になります。

`app.vue`でフォームのデータを送る処理は postBookmark の部分になります。ここでフォームに入力されたデータをaxiosを使って`/api/bookmarks_controller.rb`にデータを送信しています。


### ブックマーク編集・更新機能

#### bookmarks_controllerの編集

```app/controllers/api/bookmarks_controller.rb
class Api::BookmarksController < ApplicationController
  protect_from_forgery :except => [:create, :update, :destroy]

  # updateアクションを追加
  def update
    @bookmark = Bookmark.find(params[:id])
    if @bookmark.update_attributes(bookmark_params)
      render "index", formats: :json, handlers: "jbuilder"
    else
      render json: @bookmark.errors, status: :unprocessable_entity
    end
  end


  private
    def bookmark_params
      params.require(:bookmark).permit(:title, :url, :category)
    end
end
```

#### app.vueの編集

【テンプレート部分】

```app/javascript/app.vue
<template>
  <v-app id="app">

   <!------- 中略 -------->

   <v-flex xs8>
      <div style="width: 100%; margin: 5px 0 20px 0; display: flex; justify-content: center;">
        <h1>Bookmark 一覧</h1>
      </div>
      <v-layout>
        <v-flex row wrap style="justify-content: center;">

        <!-- bookmark の表示部分 -->
          <v-card v-for="bookmark in bookmarkList" :key="bookmark.id" style="width: 100%">
            <v-card-title primary-title style="margin-bottom: 15px; width: 100%; padding-bottom: 10px;">
             <div style="width: 100%;">
                <div class="headline mb-0" style="display: flex; justify-content: space-between; width: 100%">
                  <p style="font-size: 18px;">
                    {{ bookmark.title }}  <!-- タイトルを表示 -->
                  </p>

                  <!-- 編集用フォームのモーダルを開くボタン -->
                  <v-tooltip right>
                    <template v-slot:activator="{ on }">
                      <!-- togglePutModalに編集するbookmarkのIDを引数で渡す -->
                      <v-btn light v-on="on" @click="togglePutModal(bookmark.id)" style="margin-bottom: 8px">
                        <span class="material-icons" style="margin-right: 4px;">create</span>
                      </v-btn>
                    </template>
                    <span>編集する</span>
                  </v-tooltip>

                </div>
                <v-divider></v-divider>
                <div style="font-size: 16px; display: flex; justify-content: space-between; width: 100%">
                  <div>
                    #{{ bookmark.category }}  <!-- カテゴリーを表示 -->
                  </div>
                </div>
              </div>
            </v-card-title>
          </v-card>

        </v-flex>
       </v-layout>
    </v-flex>

   <!------- 中略 -------->

   <!-- 更新用モーダルウィンドウ -->
    <v-dialog v-model="dialogPutFlag" width="500px" persistent>
      <v-card>
        <v-card-title class="headline orange darken-4 white--text" primary-title>
          ブックマーク編集
        </v-card-title>

        <v-text-field v-model="putTitle" :counter="50" label="Title" required style='margin:20px;'></v-text-field> 
        <v-text-field v-model="putUrl" label="URL" required style='margin:20px;'></v-text-field> 
        <v-text-field v-model="putCategory" :counter="50" label="Category" required style='margin:20px;'></v-text-field> 
        <v-select v-model='putCategory' :items="categoriesForEdit" label="Category [select]" style='margin:20px;'></v-select>

        <v-divider></v-divider>
        <v-card-actions>
          <!-- キャンセルボタン cancelメソッドを呼び出す -->
          <v-btn dark @click="cancel">
            Cancel
          </v-btn>

          <v-spacer></v-spacer>

          <!-- 更新ボタン putBookmarkメソッドを呼び出し submit -->
          <v-btn @click="putBookmark">
            Update
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

　　<!------- 中略 -------->

  </v-app> 
</template>
```

【Vue.js部分】

```app/javascript/app.vue
<script>
import axios from 'axios';


export default {
  data: function () {
    return {
      bookmarkList: ['',''],
      allData: ['',''],
      categories: ['All'],
      categoriesForEdit: [],
      category: 'ALL',

      dialogPostFlag: false,  // 新規投稿用モーダルウィンドウ
      postTitle: "",
      postUrl: "",
      postCategory: "",

      dialogPutFlag: false,  //編集用モーダルウィンドウ
      putTitle: '',
      putUrl: '',
      putCategory: '',
    }
  },
  mounted () {
    this.setBookmark();
  },
  methods: {
    setBookmark: function () {
      axios.get('/api/bookmarks')
      .then(response => {
        this.allData = response.data
        this.bookmarkList = this.allData
        }
      );
      this.listCategories();
    },

    listCategories: function() {
      this.categories = []
      this.categoriesForEdit = []
      this.categories.push('ALL')
      var i = 0;
      for (i=0; i<this.allData.length; i++) {
        if (this.categories.indexOf(this.allData[i].category) == -1) {
          this.categories.push(this.allData[i].category)
          this.categoriesForEdit.push(this.allData[i].category)
        }
      }
    },

    togglePostModal: function() {
      this.dialogPostFlag = !this.dialogPostFlag
    },
    postBookmark: function() {
      axios.post('/api/bookmarks', {title:this.postTitle,url:this.postUrl,category:this.postCategory})
        .then(response => {
          this.setBookmark();
          this.postTitle = ''
          this.postUrl = ''
          this.postCategory = ''
        }
      );
      this.dialogPostFlag = !this.dialogPostFlag
    },

    // 編集用モーダルウィンドウの開閉を操作するメソッド
    togglePutModal: function(id) {  // 編集するbookmarkのIDを受け取る
      this.id = id

      // axiosを使って編集対象となるbookmarkの内容を取得し、フォームに表示させる
      axios.get(`/api/bookmarks/${this.id}.json`)
        .then(response => {
          this.putTitle = response.data.title
          this.putUrl = response.data.url
          this.putCategory = response.data.category
        }
      );
      // モーダルの開閉状態を操作
      this.dialogPutFlag = !this.dialogPutFlag
    },

    // 編集したブックマークの内容を更新するメソッド
    putBookmark: function() {
      axios.put(`/api/bookmarks/${this.id}`, {title:this.putTitle, url:this.putUrl, category:this.putCategory})
        .then(response => {
          this.setBookmark();
        }
      );
      this.dialogPutFlag = !this.dialogPutFlag
    },

    // 更新せずにそのままモーダルを閉じるメソッド
    cancel: function() {
      this.dialogPutFlag = !this.dialogPutFlag
    },

  }
}
</script>
```

以上がブックマークの編集、更新機能の実装部分になります。

axiosを使って更新する際は、putを使い、`/api/bookmarks/${this.id}`にそれぞれのカラムに更新する内容を代入して送信します。

`/api/bookmarks/${this.id}`の部分は、ターミナルで`rails routes`コマンドを実行して確認できます。

```
% rails routes
.
.
.
api_bookmark GET    /api/bookmarks/:id(.:format)     api/bookmarks#show {:format=>/json/}
             PATCH  /api/bookmarks/:id(.:format)     api/bookmarks#update {:format=>/json/}
             PUT    /api/bookmarks/:id(.:format)     api/bookmarks#update {:format=>/json/}
             DELETE /api/bookmarks/:id(.:format)     api/bookmarks#destroy {:format=>/json/}
```


### ブックマーク削除機能

#### bookmarks_controllerの編集

```app/controllers/api/bookmarks_controller.rb
class Api::BookmarksController < ApplicationController
  protect_from_forgery :except => [:create, :update, :destroy]

  # destroyアクションを追加
  def destroy
    @bookmark = Bookmark.find(params[:id])
    if @bookmark.destroy
      render json: { json: 'Bookmark was successfully deleted.'}
    else
      render json: @bookmark.errors, status: :unprocessable_entity
    end
  end

end
```

#### app.vueの編集

【テンプレート部分】

```app/javascript/app.vue

<template>
  <v-app id="app">

   <!------- 中略 -------->

   <v-flex xs8>
      <div style="width: 100%; margin: 5px 0 20px 0; display: flex; justify-content: center;">
        <h1>Bookmark 一覧</h1>
      </div>
      <v-layout>
        <v-flex row wrap style="justify-content: center;">

        <!-- bookmark の表示部分 -->
          <v-card v-for="bookmark in bookmarkList" :key="bookmark.id" style="width: 100%">
            <v-card-title primary-title style="margin-bottom: 15px; width: 100%; padding-bottom: 10px;">
             <div style="width: 100%;">
                <div class="headline mb-0" style="display: flex; justify-content: space-between; width: 100%">
                  <p style="font-size: 18px;">
                    {{ bookmark.title }}  <!-- タイトルを表示 -->
                  </p>
                  <!-- 編集用フォームのモーダルを開くボタン -->
                  <v-tooltip right>
                    <template v-slot:activator="{ on }">
                      <!-- togglePutModalに編集するbookmarkのIDを引数で渡す -->
                      <v-btn light v-on="on" @click="togglePutModal(bookmark.id)" style="margin-bottom: 8px">
                        <span class="material-icons" style="margin-right: 4px;">create</span>
                      </v-btn>
                    </template>
                    <span>編集する</span>
                  </v-tooltip>

                </div>
                <v-divider></v-divider>
                <div style="font-size: 16px; display: flex; justify-content: space-between; width: 100%">
                  <div>
                    #{{ bookmark.category }}  <!-- カテゴリーを表示 -->
                  </div>

                  <!-- 削除確認のモーダルを開くボタン -->
                  <v-tooltip right>
                    <template v-slot:activator="{ on }">
　　　　　　　　　　　　　<!-- toggleDeleteModalに削除するbookmarkのIDを引数で渡す -->
                      <v-btn dark v-on="on" @click="toggleDeleteModal(bookmark.id)" style="margin-top: 8px">
                        <span class="material-icons" style="margin-right: 4px;">delete</span>
                      </v-btn>
                    </template>
                    <span>削除する</span>
                  </v-tooltip>

                </div>
              </div>
            </v-card-title>
          </v-card>
        </v-flex>
       </v-layout>
    </v-flex>

   <!------- 中略 -------->

　　<!-- 削除確認用モーダル -->
    <v-dialog v-model="dialogDeleteFlag" width="400">
      <v-card>
        <v-card-title class="headline blue-grey darken-3 white--text" primary-title>
          確認
        </v-card-title>
        <br>
        <br>
        <v-card-text>
          <p>本当に削除してもよろしいですか？</p>
        </v-card-text>
        <v-divider></v-divider>
        <v-card-actions>
          <v-spacer></v-spacer>
          <v-btn dark class="yellow--text" @click="deleteBookmark()">
            Delete
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>

  </v-app> 
</template>
```

【Vue.js部分】

```app/javascript/app.vue
<script>
import axios from 'axios';


export default {
  data: function () {
    return {
      bookmarkList: ['',''],
      allData: ['',''],
      categories: ['All'],
      categoriesForEdit: [],
      category: 'ALL',

      dialogPostFlag: false,  // 新規投稿用モーダルウィンドウ
      postTitle: "",
      postUrl: "",
      postCategory: "",

      dialogPutFlag: false,  //編集用モーダルウィンドウ
      putTitle: '',
      putUrl: '',
      putCategory: '',

      dialogDeleteFlag: false,  // 削除していいかどうかを確認するモーダルウィンドウ
    }
  },
  mounted () {
    this.setBookmark();
  },
  methods: {
    setBookmark: function () {
      axios.get('/api/bookmarks')
      .then(response => {
        this.allData = response.data
        this.bookmarkList = this.allData
        }
      );
      this.listCategories();
    },

    listCategories: function() {
      this.categories = []
      this.categoriesForEdit = []
      this.categories.push('ALL')
      var i = 0;
      for (i=0; i<this.allData.length; i++) {
        if (this.categories.indexOf(this.allData[i].category) == -1) {
          this.categories.push(this.allData[i].category)
          this.categoriesForEdit.push(this.allData[i].category)
        }
      }
    },

    togglePostModal: function() {
      this.dialogPostFlag = !this.dialogPostFlag
    },
    postBookmark: function() {
      axios.post('/api/bookmarks', {title:this.postTitle,url:this.postUrl,category:this.postCategory})
        .then(response => {
          this.setBookmark();
          this.postTitle = ''
          this.postUrl = ''
          this.postCategory = ''
        }
      );
      this.dialogPostFlag = !this.dialogPostFlag
    },

    togglePutModal: function(id) {
      this.id = id
      axios.get(`/api/bookmarks/${this.id}.json`)
        .then(response => {
          this.putTitle = response.data.title
          this.putUrl = response.data.url
          this.putCategory = response.data.category
        }
      );
      this.dialogPutFlag = !this.dialogPutFlag
    },
    putBookmark: function() {
      axios.put(`/api/bookmarks/${this.id}`, {title:this.putTitle, url:this.putUrl, category:this.putCategory})
        .then(response => {
          this.setBookmark();
        }
      );
      this.dialogPutFlag = !this.dialogPutFlag
    },
    cancel: function() {
      this.dialogPutFlag = !this.dialogPutFlag
    },

    // 削除の確認をするモーダルの開閉状態を管理するメソッド
    toggleDeleteModal: function(id) {
      this.id = id
      this.dialogDeleteFlag = !this.dialogDeleteFlag
    },

    // bookmarkを削除するメソッド
    deleteBookmark: function() {
      axios.delete(`/api/bookmarks/${this.id}`)
        .then(response => {
          this.setBookmark();
        }
      );
      this.dialogDeleteFlag = !this.dialogDeleteFlag
    },
  }
}
</script>
```

以上がブックマークの削除機能の実装部分になります。
これで一通りのブックマークの関するCRUDを実装することができました。

#### bookmarks_controllerを整える
最後に、bookmarks_controllerで共通している処理をまとめます。

```app/controllers/api/bookmarks_controller.rb
class Api::BookmarksController < ApplicationController
  before_action :set_bookmark, only: [:update, :destroy]
  protect_from_forgery :except => [:create, :update, :destroy]

  def index
    @bookmarks = Bookmark.order('created_at DESC')
    render "index", formats: :json, handlers: "jbuilder"
  end

  def create
    @bookmark = Bookmark.new(bookmark_params)
    if @bookmark.save
      render :show, status: :created
    else
      render json: @bookmark.errors, status: :unprocessable_entity
    end
  end

  def update
    if @bookmark.update_attributes(bookmark_params)
      render "index", formats: :json, handlers: "jbuilder"
    else
      render json: @bookmark.errors, status: :unprocessable_entity
    end
  end

  def destroy
    if @bookmark.destroy
      render json: { json: 'Bookmark was successfully deleted.'}
    else
      render json: @bookmark.errors, status: :unprocessable_entity
    end
  end
 
  private
    def bookmark_params
      params.require(:bookmark).permit(:title, :url, :category).merge(user_id: current_user.id)
    end

    def set_bookmark
      @bookmark = Bookmark.find(params[:id])
    end
end

```

### CSRF対策
最初にRails側でCSRF対策を無効化しましたが、改めてフロント側で対策していきます。
具体的には、csrfトークンをヘッダーに含める形で実装します。

```app/javascript/app.vue
<script>
import axios from 'axios';

// ヘッダーにcsrfトークンを含ませる
axios.defaults.headers.common = {
    'X-Requested-With': 'XMLHttpRequest',
    'X-CSRF-TOKEN' : document.querySelector('meta[name="csrf-token"]').getAttribute('content')
};

export default {
  data: function () {
    return {
      ...
```

参考にさせていただいた記事：

https://qiita.com/ngron/items/2faae8068baa093d6aba

### ドラッグ&ドロップでの並び替え機能を実装する

Vueには、ドラッグ&ドロップの実装に便利な Vue.Draggable というライブラリがあります。
今回はこの Vue.Draggable を公式Githubの Readme を参考に導入し実装します。

https://github.com/SortableJS/Vue.Draggable

```
% yarn add vuedraggable
```

【テンプレート部分を編集】
ドラッグ&ドロップさせたい要素は基本的に`<draggable>`の直下に配置ないとドラッグ&ドロップできないため、`<draggable></draggable>`で挟み込むように記述します。

```app/javascript/app.vue
<template>
  <v-app id="app">
    <v-container style="height: 1000px; max-width: 2400px; padding: 0 20px;">
      <v-layout>
        
        <!------- 中略 -------->

        <v-flex xs8>
          <div style="width: 100%; margin: 5px 0 20px 0; display: flex; justify-content: center;">
            <h1>Bookmark 一覧</h1>
          </div>
          
          <v-layout>
            <v-flex row wrap style="justify-content: center;">
              <!-- bookmarkList の配列の中身を入れ替えるように v-model で指定 -->
              <draggable v-model="bookmarkList" style="margin: 0 25px; width: 80%; cursor: pointer;">
                <v-card v-for="bookmark in bookmarkList" :key="bookmark.id" :items-per-page="itemsPerPage" style="width: 100%">
                  
                <!------- 中略 -------->

                </v-card>
             </draggable>

    </v-container>
  </v-app> 
</template>
```

【Vue.js側を編集】
Vue.Draggable をインポートし、コンポーネントとして扱います。

```app/javascript/app.vue
<script>
import draggable from 'vuedraggable' // Vue.Draggable をインポート
import axios from 'axios';


export default {
  ...

  components: {
    draggable,  // draggableをコンポーネントとして使う
  },
  methods: {
  ...
```

以上で本記事【ブックマークのCRUD + ドラッグ&ドロップ機能の実装】は終了です。

次章、フリーワード検索、カテゴリー別絞り込み機能の実装はこちら↓

https://qiita.com/showy/items/7ca2e46e09371c489204