この記事で説明する事
- SJRを使用して非同期ページネーション実装する。
- ページネーション自体はgemの
kaminari
を使用する。
簡単にアプリの作りを説明
インスタのクローンアプリで、ユーザーがいて、画像を投稿する事が出来る様な仕組みになっています。
user has many posts
の形になっていて、今回はPostsに対してページネーションを付け様と思います。参考までにテーブル構成乗っけてます。
usersテーブル
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テーブル
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
gem 'kaminari'
いつも通りbundle install
今回はユーザーが投稿したpostsにページネーションを付けるのでposts_controller
の記述を変更する。
posts_controller
def index
@posts = Post.page(params[:page]).includes(:user).order(created_at: :desc)
end
ここまではテーブル構成が同じであれば、非同期でも同期でも同じになリます。
これでページネーションのボタンを押せば自動的に対応するpostsを取ってきてくれるようになります。
通常なら後はviewにpaginate @posts
と記述するだけで普通のページネーションが実装できます。
ただ今回は、非同期ページネーションなのでここからが少し違う実装になります。
※そもそも非同期ってなんぞや!って人はこの記事がめちゃわかりやすいので貼っておきます。
初心者目線でAjaxの説明
実装のイメージ
今回の実装イメージを説明しておきます。
上の記事を読んでいただいた方は、分かると思いますが非同期処理はものすごく雑に言うと
ページ全体ではなく、必要な部分だけ画面に反映させると言えます。
なので今回の要件に当てはめると
①反映させる画面をまず決める。今回はposts/index.html.slim
②①で決めたファイルの中で。ポスト一覧部分だけを特定する(今回はidを付与して特定する。)
③posts/index.html.slim
から先ほど特定したpost一覧部分だけを、スバっと入れ替える。(この部分だけレスポンスを返してあげる)
このようなイメージで実装していきたいと思います!!
実装
では先ほどのイメージ通りに行きたいと思います。
①反映させる画面をまず決める
今回はpostの一覧画面に反映させるのでposts/index.html.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
.container
.row
.col-md-8.col-12
= render @posts <= この部分がpost一覧
= paginate @posts <= ページネーション
つまりこのパーシャルとページネーションの部分を非同期通信でスバっと入れ替えればいい事が分かります。
ではどうやってこの2つの部分を特定するのか?
このような場合はid
やclass
を付与して、入れ替える場所の目安を作ってあげましょう。
今回は#posts
というid
を親要素に付与します。
※後々にJSでQuerySelector
やgetElementbyId
メソッドを使用して場所が特定できます。
.container
.row
#posts.col-md-8.col-12
= render @posts <= この部分がpost一覧
= paginate @posts <= これはページネーション
ちょうどこの部分です。
#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
.container
.row
#posts.col-md-8.col-1
= render @posts
= paginate @posts, remote: true
これでJS形式のリクエストを送信する事が出来る様になりました。
またページネーションのボタンを押した時に自動的に呼び出されるテンプレートファイルもアクション名.js.erb
が呼び出されるようになります。
このままではどこまで入れ替えるのかちょっと分かり辛いのでこの2つをパーシャルにまとめてしまいます。
posts/_posts_paginate.html.slim
= render posts
= paginate posts, remote: true
posts/index.html.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
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
メソッドを使用して、子要素を入れ替えています。今回子要素は
#posts.col-md-8.col-12
= render 'posts_paginate', posts: @posts
パーシャル= render 'posts_paginate', posts: @posts
だけになります。
つまり2行目では同じ内容で@posts
の中身だけkaminari
によって置き換えられているという事です。
3行目では、リロードの処理をするために、replaceState
というメソッドを使用してURLを書き換えています。ちなみに@page
の中身は
def index
# kaminariの処理
@page = params[:page]
end
です。ここは本筋とちょっと外れるので説明は割愛して、リンクだけ貼っておきます。
JavaScriptでURLを操作するメモ
ここまで実装出来たら非同期でページネーションが動くようになっているはずです。
非同期処理と言うと、最初はハマりがちなところなのですが
①id
で置き換える場所を特定する。
②置き換えたい場所をできればパーシャルにしてしまう。(した方がjsのメソッドで置き換えが簡単)
③js.erb
の中でパーシャル部分をそのまま置き換える。
この考え方で実装するようになってから僕はRailsのSJRに関しては簡単に実装できる様になりました。
最後に参考資料も貼っておくので良かったら読んでみてください。
参考資料
差し替える場所を特定するには
querySelectorメソッドの使い方
【JavaScript入門】getElementByIdを完全理解する3つのコツ!