0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-04-02

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

本記事の概要

完成版イメージ

今回実装する機能(CRUD)の完成イメージです。

Image from Gyazo

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

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

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

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

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

【ブックマーク削除】

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

【並び替え機能】

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

記事構成

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

本記事の参考URL

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

実装手順(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ガイド

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

それでは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 {
      ...

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

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

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

% 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 + ドラッグ&ドロップ機能の実装】は終了です。

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?