Kaminariを使用した非同期ページネーションの作り方

## この記事で説明する事

1. SJRを使用して非同期ページネーション実装する。
2. ページネーション自体はgemの`kaminari`を使用する。

## 簡単にアプリの作りを説明

インスタのクローンアプリで、ユーザーがいて、画像を投稿する事が出来る様な仕組みになっています。
`user has many posts`の形になっていて、今回はPostsに対してページネーションを付け様と思います。参考までにテーブル構成乗っけてます。

`usersテーブル`


```ruby
  create_table "users", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.string "email", null: false
    t.string "crypted_password"
    t.string "salt"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.string "username", null: false
    t.string "avatar"
    t.index ["email"], name: "index_users_on_email", unique: true
  end

```

`postsテーブル`

```ruby
  create_table "posts", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
    t.string "images", null: false
    t.text "body", null: false
    t.bigint "user_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["user_id"], name: "index_posts_on_user_id"
  end
```

## 何はともあれkaminariを導入する。

`Gemfile`

```ruby
gem 'kaminari'
```

いつも通り`bundle install`

今回はユーザーが投稿したpostsにページネーションを付けるので`posts_controller`の記述を変更する。

`posts_controller`

```ruby
def index
  @posts = Post.page(params[:page]).includes(:user).order(created_at: :desc)
end
```

ここまではテーブル構成が同じであれば、非同期でも同期でも同じになリます。
これでページネーションのボタンを押せば自動的に対応するpostsを取ってきてくれるようになります。
通常なら後はviewに`paginate @posts`と記述するだけで普通のページネーションが実装できます。

ただ今回は、非同期ページネーションなのでここからが少し違う実装になります。


※そもそも非同期ってなんぞや！って人はこの記事がめちゃわかりやすいので貼っておきます。
[初心者目線でAjaxの説明](https://qiita.com/hisamura333/items/e3ea6ae549eb09b7efb9)

## 実装のイメージ
今回の実装イメージを説明しておきます。

上の記事を読んでいただいた方は、分かると思いますが非同期処理はものすごく雑に言うと
**ページ全体ではなく、必要な部分だけ画面に反映させる**と言えます。

なので今回の要件に当てはめると

①反映させる画面をまず決める。今回は`posts/index.html.slim`
②①で決めたファイルの中で。ポスト一覧部分だけを**特定**する(今回はidを付与して特定する。)
③`posts/index.html.slim`から先ほど特定したpost一覧部分だけを、スバっと入れ替える。（この部分だけレスポンスを返してあげる）

このようなイメージで実装していきたいと思います!!

## 実装

では先ほどのイメージ通りに行きたいと思います。

### ①反映させる画面をまず決める
今回はpostの一覧画面に反映させるので`posts/index.html.slim`になります。

#### ページ全体の画像
[![Image from Gyazo](https://i.gyazo.com/aac0b3cfd975acf682910f2856fef978.jpg)](https://gyazo.com/aac0b3cfd975acf682910f2856fef978)

#### コード

```slim
.container
  .row
    #posts.col-md-8.col-12
      = render @posts
      = paginate @posts
    .col-md-4.col-12
      - if logged_in?
        .profile-box.mb-3
          = image_tag 'profile-placeholder.png', size: '50x50', class: 'rounded-circle mr-1'
          = current_user.username
      .users-box
        .card
          .card-header
            | ユーザー
          .card-body
            - @dummy_names.each do |name|
              .user.mb-3
                = image_tag 'profile-placeholder.png', size: '40x40', class: 'rounded-circle mr-1'
                = name
          .card-footer
            = link_to 'すべて見る', '#'
```
このコードが①で説明した部分的に入れ替えられる元になるコードです。
ではこの中から更新したい部分がどこか調べていきましょう。

### ②①の画像の中でポスト一覧だけを特定する

コードを良く読んでみるとpost一覧はそっくりそのままパーシャルに切り出されているのが分かりました！（4行目）
5行目にはページネーション の記述を予め追加しています。

`posts/index.html.slim`

``` slim
.container
  .row
    .col-md-8.col-12
      = render @posts  <= この部分がpost一覧
      = paginate @posts <= ページネーション
```


つまりこのパーシャルとページネーションの部分を非同期通信でスバっと入れ替えればいい事が分かります。
ではどうやってこの2つの部分を特定するのか？

このような場合は`id`や`class`を付与して、入れ替える場所の目安を作ってあげましょう。
今回は`#posts`という`id`を親要素に付与します。
※後々にJSで`QuerySelector`や`getElementbyId`メソッドを使用して場所が特定できます。

```slim
.container
  .row
    #posts.col-md-8.col-12
      = render @posts  <= この部分がpost一覧
      = paginate @posts <= これはページネーション
```
ちょうどこの部分です。

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


`#posts`がついているのが分かりますね！これで非同期処理で入れ替える場所の特定が出来ました。


### ③`posts/index.html.slim`から先ほど特定したポスト一覧部分をズバっと差し替える。

ここまでで`差し替える行われるページ`と`ページ内の差し替える場所の特定`ができました。
なので非同期処理を実装していきましょう。

`kaminari`で非同期処理を行うのは、非常に簡単です。おなじみの`remote: true`を付けるだけでjs形式のリクエストを送信する事が出来ます。話が少しそれますが、この実装が終わった後、開発者ツールの`Network`タグの`ContentType`を見てみましょう。`remote: true`にするとこの項目が`html`から`javascript`に変更されます。

```
remote: trueなし
Content-Type: text/html; charset=utf-8

remote: true あり
Content-Type: text/javascript; charset=utf-8
```

すこし寄り道しましたが、下記の様に`remote:true`を付けます

`posts/index.html.slim`

```slim
.container
  .row	  
    #posts.col-md-8.col-1
      = render @posts
      = paginate @posts, remote: true
```
これでJS形式のリクエストを送信する事が出来る様になりました。
またページネーションのボタンを押した時に自動的に呼び出されるテンプレートファイルも`アクション名.js.erb`が呼び出されるようになります。

このままではどこまで入れ替えるのかちょっと分かり辛いのでこの2つをパーシャルにまとめてしまいます。

`posts/_posts_paginate.html.slim`

```slim
= render posts
= paginate posts, remote: true
```

`posts/index.html.slim`

``` slim

.container
  .row
    #posts.col-md-8.col-12
      = render 'posts_paginate', posts: @posts
```

これで`_posts_paginate.html.slim`を差し替える処理を`index.js.erb`に書けばいいという事になります。


`posts/index.js.erb`

```js
var postsPaginate = document.querySelector('#posts'); //ここで設定したidを特定。constでも動くかも
postsPaginate.innerHTML = "<%= j render 'posts_paginate', posts: @posts%> "//innerHTMLで中身を置き換える。
history.replaceState( "", "" ,"?page=<%= @page %>");//リロードできる様にURLを書き換えている
```

1行目で場所の特定をしています。つまり`#posts`の場所を指定しています。
2行目で`innerHTML`メソッドを使用して、子要素を入れ替えています。今回子要素は

```slim
    #posts.col-md-8.col-12
      = render 'posts_paginate', posts: @posts
```

パーシャル`= render 'posts_paginate', posts: @posts`だけになります。
つまり2行目では同じ内容で`@posts`の中身だけ`kaminari`によって置き換えられているという事です。
3行目では、リロードの処理をするために、`replaceState`というメソッドを使用してURLを書き換えています。ちなみに`@page`の中身は

```ruby
def index
　# kaminariの処理
　@page = params[:page]
end
```
です。ここは本筋とちょっと外れるので説明は割愛して、リンクだけ貼っておきます。
[JavaScriptでURLを操作するメモ](https://qiita.com/PianoScoreJP/items/fa66f357419fece0e531)

ここまで実装出来たら非同期でページネーションが動くようになっているはずです。

非同期処理と言うと、最初はハマりがちなところなのですが
①`id`で置き換える場所を特定する。
②置き換えたい場所をできればパーシャルにしてしまう。（した方がjsのメソッドで置き換えが簡単）
③`js.erb`の中でパーシャル部分をそのまま置き換える。

この考え方で実装するようになってから僕はRailsのSJRに関しては簡単に実装できる様になりました。
最後に参考資料も貼っておくので良かったら読んでみてください。

## 参考資料

### 差し替える場所を特定するには
[querySelectorメソッドの使い方](https://wp-p.info/tpl_rep.php?cat=js-intermediate&fl=r4)
[【JavaScript入門】getElementByIdを完全理解する3つのコツ！](sejuku.net/blog/27019)

### 差し替える為のメソッド

[【JavaScript入門】innerHTMLでdivタグ内の要素を取得、設定する方法](https://www.sejuku.net/blog/26442)