【Rails6 + Vue.js】ブックマークを管理する簡単なSPAを作ってみた話（第四章：ローディング表示、ページネーション、ユーザー管理機能の実装）

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

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

## 本記事の概要
### 今回実装する機能について
【ローディング表示機能】

- vue-loading-template を導入してローディングを表示させます。

【ページネーション機能】

- gem で kaminari を導入して実装します。

【ユーザー管理機能】

- 今回は devise で実装します。

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

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

### 本記事の参考URL
本記事の作成にあたって主に以下のドキュメント・記事を参考にさせて頂きました。

- [【Vue.js】ローディング画面の実装方法（サンプルコード付き）](https://qiita.com/terufumi1122/items/214d82f74370ef377aa2)
- [vue-loading (Github)](https://github.com/jkchao/vue-loading)
- [【rails】kaminariを使ってページネーションを作る](https://qiita.com/you8/items/df68aaee3010e282d1ae)
- [Rails + Vue.js でページネーション付きのテーブルを簡単作成](https://qiita.com/iPzYN2gLSV2HeSS/items/0dbd4d23baf36f9fc06f?utm_campaign=popular_items&utm_medium=feed&utm_source=popular_items)

## 実装
それでは実装に移ります。

### ローディング表示機能

vue-loading-templateを導入して実装します。
参考記事↓

https://qiita.com/terufumi1122/items/214d82f74370ef377aa2

```
% yarn add vue-loading-template
```

#### app.vueを編集

```app/javascript/app.vue
<template>
  <v-app id="app">
  <!-- ローディングを表示 -->
  <Loading v-show="isLoading"></Loading>
    <v-container>

      <!-- 中略 -->

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

<script>
import draggable from 'vuedraggable'
import Loading from './components/Loading'  // Loading コンポーネントをインポート
import axios from 'axios';

export default {
  data: function () {
    return {
      isLoading: true,
      bookmarkList: ['',''],
      allData: ['',''],
      
      ...

    }
  },
  mounted () {
    axios.get("/api/bookmarks")
    .then(response => {
        this.allData = response.data;
        this.isLoading = false;  // マウントしたらfalseに変更
      }
    );
    this.setBookmark();
  },
  components: {
    Loading,  // Loagingコンポーネントを追加
    draggable,
  },
  methods: {
  ...

  }
}
</script>
```

#### components フォルダ配下に Loading.vue を設置して実装

```app/javascript/components/Loading.vue
<template>
  <div v-show="isLoading">
    <div class="fullview">
      <div class="loading-spacer"></div>
      <!-- spiningDubblesというタイプのローディングを実装 -->
      <vue-loading 
        type="spiningDubbles"
        color="#2f8af0"
        :size="{ width: '100px', height: '100px' }"
        >
      </vue-loading>
    </div>
  </div>
</template>

<script>
import { VueLoading } from 'vue-loading-template'

export default {
  name: 'isLoading',
  data() {
    return {
      isLoading: true,
    }
  },
  components: {
    VueLoading,
  },

}
</script>

<style>
.fullview {
  width: 100%;
  height: 100%;
  background: rgba(254, 254, 254, 0.8);
  position: fixed;
  top: 60px;
  left: 0;
  z-index: 5;
}
.loading-spacer {
  height: 30%;
}
</style>
```

以上でローディング表示機能の実装は終了です。


### ページネーション機能

kaminariを導入して実装します。
参考記事↓

https://qiita.com/iPzYN2gLSV2HeSS/items/0dbd4d23baf36f9fc06f?utm_campaign=popular_items&utm_medium=feed&utm_source=popular_items

```Gemfile
gem 'kaminari'
```

```
% bundle install
```

#### bookmarks_controllerの編集

```app/controllers/api/bookmarks_controller.rb
class Api::BookmarksController < ApplicationController

  def index
    page = params[:page] || 1  # 表示するページの指定
    per = params[:per] || 30   # 1ページあたり30件のブックマークを表示

    @bookmarks = Bookmark.order('created_at DESC').page(page).per(per)
    total_pages = @bookmarks.total_pages  # 全ページ数を取得

    response = {
      bookmarks: @bookmarks,
      total_pages: total_pages,
    }
    render json: response
  end

end
```

#### app.vueの編集

【 Vue.js の編集】

```app/javascript/app.vue
<script>
export default {
  data: function () {
    return {
      isLoading: true,
      bookmarkList: ['',''],

      ...

      currentPage: 1,    // 表示するページの番号
      itemsPerPage: 30,  // １ページあたりの表示件数
      totalPages: null,  // ページネーションした時の全ページ数
    }
  },

  ...

  methods: {
    setBookmark: function () {
      // Axios で api にリクエスト
      const url = `/api/bookmarks?page=${this.currentPage}?per=${this.itemsPerPage}`;
      axios.get(url)
      .then(response => {
        // ブックマークの全データを取得
        this.allData = response.data.bookmarks
        this.bookmarkList = this.allData

        // ページネーションの全ページ数を取得
        this.totalPages = response.data.total_pages

        this.listCategories();
        this.abstruct();
        }
      );
    },

    ...

  }
}
</script>
```

【template の編集】

```app/javascript/app.vue
<template>
  <v-app id="app">
  <Loading v-show="isLoading"></Loading>
    <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;">
              <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-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%">
                        <a v-bind:href="bookmark.url" target="_blank" rel="noopener noreferrer" style="font-size: 18px;">
                          {{ bookmark.title }}
                        </a>
                        
                        <v-tooltip right>
                          <template v-slot:activator="{ on }">
                            <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 }">
                            <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>
              </draggable>
            </v-flex>
          </v-layout>

          <!-- ページネーション表示部分を追加 -->
          <div class="text-xs-center" style="margin: 20px 0 40px 0;">
            <v-pagination
              v-model="currentPage"
              :length="totalPages"
              @input="setBookmark"
            ></v-pagination>
          </div>
        </v-flex>

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

以上でページネーション機能の実装は終了です。

### ユーザー管理機能の実装
deviseを導入して実装します。（新規登録ページやログインページの実装に関しては割愛します。）
本来は最初に実装するべきですが、今回はブックマークの機能(Vue.js)を主な目的としているため後から実装しています。

```
gem 'devise'
```

`bundle install`してdeviseをインストール

```
% bundle install
% rails g devise:install
```

bookmarkのマイグレーションファイルをロールバックして編集

```db/migrate/2021xxxxxxxxx2_create_bookmarks.rb
class CreateBookmarks < ActiveRecord::Migration[6.0]
  def change
    create_table :bookmarks do |t|
      t.string :title, null: false
      t.string :url, null: false
      t.string :category, null: false
      t.references :user, foreign_key: true
      t.timestamps
    end
  end
end
```

userモデルの作成

```
% rails g devise user
```

```db/migrate/2021xxxxxxxxxx1_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :nickname,            null: false, default: ""
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at
      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
  end
end
```

```
% rails db:migrate
```

`application_controller`の編集

```app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  # 登録に必要なストロングパラメータを設定
  before_action :configure_permitted_parameters, if: :devise_controller?
  # ユーザーがログインしていない場合はログインページを表示
  before_action :authenticate_user!

  private
  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:nickname])
  end
end
```

それぞれのモデルを編集（アソシエーションの記述）

```app/models/user.rb
class User < ApplicationRecord
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  has_many :bookmarks  # 一人のユーザーは複数のブックマークのデータを扱う

  # バリデーション設定
  validates :nickname, presence: true
  PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?[\d])[a-z\d]+\z/i.freeze
  validates_format_of :password, with: PASSWORD_REGEX, message: 'には英字と数字の両方を含めて設定してください'
end
```

```app/models/bookmark.rb
class Bookmark < ApplicationRecord
  belongs_to :user
  
  with_options presence: true do
    validates :url, format: {with: /\A#{URI::regexp(%w(http https))}\z/}
  end
end
```

#### `bookmarks_controller`の編集

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

  def index
    page = params[:page] || 1
    per = params[:per] || 30

    # ログインしているユーザーのブックマーク情報を取得
    @bookmarks = Bookmark.where(user_id: current_user.id).order('created_at DESC').page(page).per(per)
    total_pages = @bookmarks.total_pages

    response = {
      bookmarks: @bookmarks,
      total_pages: total_pages,
    }
    render json: response
  end
end
```

以上でユーザー管理機能の実装は終了です。
また、今後学習を深めてJsonWebToken(JWT)によるユーザー認証機能の実装にも挑戦したいと思います。

### 最後に
以上でブックマーク個人管理アプリケーションの実装は全て終了です。
いかがだったでしょうか？
未だ記事を書くことに慣れていない為分かりづらい部分や読みにくい部分があるかもしれませんが今後修正点や改善点を気付き次第修正をしていこうと思います。

読んでいただき、ありがとうございました。