[Rails6]モーダルの中で非同期ページネーションできるようにする

6 min read読了の目安(約5900字

こんにちは、だむはです。
sister(シスター)」というサービスを個人開発しています。

先週、通知一覧の機能を追加したのですが、モーダルの中のページネーションに苦戦したので、メモとして記事にしてみます。

通知一覧出来上がりはこれ

1、モーダルで通知一覧

2、モーダル内にページネーション

2、非同期でページネーション

要件

  • Rails6
  • BootStrap(モーダル)
  • kaminari(ページネーション)

※BootStrapとkaminariの導入説明は省きます

実装方法

1、Controllerにデータ取得処理を書く

今回は、モーダル内でのページネーションについてなので、Controllerでのデータ取得条件の説明は省きます。

notification_controller.rb
    def index
    
      #通知データを取得
        @notifications = current_user.passive_notifications.page(params[:page])
	                          .per(20)
				  .order(created_at: :desc)
				  
	#通知一覧に遷移したら、既読済みにする
        @notifications.where(checked: false).each do |notification|
          notification.update_attributes(checked: true)
        end

        if params[:page].present?
          render 'pagenate.js.erb'  #2ページ目はこっち
        else
          render 'index.js.erb'  #1ページ目はこっち
        end

    end

2、通知一覧をモーダル表示させる

通知一覧呼び出しをAjax化(非同期)する

今回は通知一覧はヘッダーのにおくので、applications.html.erbに配置します。

applications.html.erb
<%= link_to '/notifications', remote: true %>

remote: trueをつけると、リクエストがhtmlではなくjsになります。
index.html.erbではなくindex.js.erbが読み込まれるようになります。

index.js.erbを作成する

index.js.erb
$("#notify-modal").html("<%= escape_javascript(render 'index', locals: { notifications: @notifications } ) %>")
$("#notify-modal").modal("show")

1行目:applications.html.erbのid=notify-modalに対して、_index.html.erbをレンダリングします。
2行目:モーダルを表示する処理です。

表示用のdivを配置する

モーダルの表示はBootStrapを使用します。
applications.html.erbにモーダル表示用のdivを配置します。
index.js.erbで記載した「#notify-modal」の部分です。

applications.html.erb
<div id="notify-modal" class="modal notify-modal fade" tabindex="-1" role="dialog" aria-hidden="true"></div>

場所はどこでも大丈夫です。

モーダル表示用パーシャル_index.html.erbを作成する

以下のようにmodal-dialogmodal-contentをクラス名にして表示したい内容をdivで囲ってあげます。

今回は、通知の表示部分はパーシャルを作成し、レンダーするようにしています。

_index.html.erb
<div class="modal-dialog modal-lg" style="z-index: 9999" role="document">
    <div class="modal-content">
        <div class="modal-header">
            <h5 class="modal-title" id="Modal">通知</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="閉じる">
            <span aria-hidden="true">&times;</span>
            </button>
        </div>
        <div class="modal-body">
        <% if @notifications.exists? %>
            <%= render partial: 'list', locals: { nitifications: @nitifications } %>
        <% else %>
            <p>通知はありません</p>
        <% end %>
        </div>
    </div>
</div>

これで、モーダル表示は完成です。

通知一覧用パーシャル_list.html.erbを作成する

ページネーションを非同期で実施するため、ここでさらにパーシャルを作成し、レンダーします。
理由はモーダル画面と共通のパーシャルを使用すると、モーダルのなかにモーダルが表示されるような処理になってしまうためです。

以下の方法で、通知部分のみが非同期でレンダーし、非同期ページネーションがうまくいくようになります。

_list.html.erb
 <%= render @notifications %>
 <%= paginate @notifications, remote: true %>

通知詳細用パーシャル_notification.html.erbを作成する

通知用のパーシャルを作成します。今回はブログが作成された際のみ通知される仕様とします。

_notification.html.erb
<% if notification.action == "blog" %>
  <span>\ お知らせ / <%= link_to "#{notification.blog.title}", notification.blog.url, target: :_blank, class: "stretched-link" %></span>
<% end %>

ここまでで、一旦、ページネーションなしのデータ表示は完成です。
ここから、モーダル内で非同期ページネーションするための処理を書いていきます。

3、モーダル内で非同期ページネーションさせる

1ページ目と2ページ目以降のレンダリング先を変更する

2ページ目以降に遷移するときは、モーダル表示してほしくありません。
そのため、冒頭でも記載している通り、Controller側でparams[:page]に値が入っている場合(ページネーションされている場合)はモーダル表示されないようにする処理を入れてあげます。

notification_controller.rb
    def index
    
      #通知データを取得
        @notifications = current_user.passive_notifications.page(params[:page])
	                          .per(20)
				  .order(created_at: :desc)
				  
	#通知一覧に遷移したら、既読済みにする
        @notifications.where(checked: false).each do |notification|
          notification.update_attributes(checked: true)
        end

        if params[:page].present?
          render 'pagenate.js.erb'  #2ページ目以降はこっち!!!!
        else
          render 'index.js.erb' 
        end

    end

これで、params[:page]に値が入っているときはpagenate.js.erbにレンダリングされます。

pagenate.js.erbを作成する

pagenate.js.erb
$("#pagenate").html("<%= escape_javascript(render 'list', locals: { notifications: @notifications } ) %>")

1行目:_index.html.erbのid=pagenateに対して、_list.html.erbをレンダリングします。

_list.html.erbにレンダリング

最初のモーダル表示の際と違い、直接_list.html.erbにレンダリングすることで、モーダル表示されることを防ぎます。

_list.html.erb
 <%= render @notifications %>
 <%= paginate @notifications, remote: true %>

_index.html.erbに

_index.html.erbはすでにモーダル表示されているパーシャルです。
_index.html.erbのclass="modal-body"の部分にid=pagenateを追加します。
pagenate.js.erbで記載した「#pagenate」の部分です。

_index.html.erb
<div class="modal-dialog modal-lg" style="z-index: 9999" role="document">
    <div class="modal-content">
        <div class="modal-header">
            <h5 class="modal-title" id="Modal">通知</h5>
            <button type="button" class="close" data-dismiss="modal" aria-label="閉じる">
            <span aria-hidden="true">&times;</span>
            </button>
        </div>
        <div class="modal-body" id="pagenate"> ←ここ!!!!
        <% if @notifications.exists? %>
            <%= render partial: 'list', locals: { nitifications: @nitifications } %>
        <% else %>
            <p>通知はありません</p>
        <% end %>
        </div>
    </div>
</div>

これで、モーダル内の非同期ページネーションができるようになります。

まとめ

モーダル内でのページネーション方法がなかなか見つからなかったので、自分で試行錯誤して実装してみました。もっといい方法があったら誰か教えてください!

参考

Rails5と BootstrapでAjax-modalform
ページネーション 同期 非同期