ピスタチオを食べながらrailsを楽しむ

ピスタチオ大好きな著者のrailsを使ったツール作成の日記です。

Kaminariをjsonでajax化する

Kaminariは便利だがネット上のソースを見るとjs.erbで対応しているものが多い。
(というかKaminariに限らずajaxはjs.erbばかりだった)ので、備忘録も兼ねて。

手順

  1. はじめに
  2. ajax
  3. リファクタリング(おまけ)

はじめに

まずは普通に実装。Kaminariを使えば超手軽にページネーションが実装できる。 このあたりの説明は割愛。

コントローラー。

# app/controllers/books_controller.rb
  def index
    @books = Book.page(params[:page]).per(10)
  end

次にビュー。パーシャル化しておく。

# app/views/books/index.html.erb
<div id="paginator">
  <%= render 'paginator' %>
</div>
<div id="books">
  <%= render 'book'  %>
</div>
# app/views/books/_paginator.html.erb
<%= paginate @books %>
# app/views/books/_book.html.erb
<% @books.each do |book| %>
  <%= book.title %>
<% end %>

ajax

ここからjsonで動くようにする。

# app/views/books/_paginator.html.erb
<%= paginate @books, remote: true %>

remote: trueでリクエストがajaxで処理されるようになる。 コントローラーではajaxの場合のみpartialを返すようにする。

# app/controllers/books_controller.rb
  def index
    @books = Book.page(params[:page]).per(10)

    # view_contextでpaginateメソッドを使いパーシャルの中身と同じものを生成
    paginator = view_context.paginate(
      @books,
      remote: true
    )

    # render_to_stringでパーシャルの中身を生成
    books = render_to_string(
      partial: 'book',
      locals: { books: @books }
    )

    #リクエストを確認しajaxだった場合にはjsonで各パーシャルをクライアントへ返す
    if request.xhr?
      render json: {
        paginator: paginator,
        books: books
      }
    end
  end

最後にcoffee(js)側でサーバから受け取った値を画面に表示する。

// app/assets/javascripts/books.js.coffee
$ ->
  // paginationのリンクからajax:successが発火したら処理を開始
  $('#paginator').on 'ajax:success', 'li a', (e, result, status, xhr) ->

    // controllerでresultの中に入れたpaginatorとbooksをそれぞれ表示する
    $('#paginator').html result.paginator
    $('#books').html result.books

これでjs.erbを書くことなくajaxでKaminariが動く。

リファクタリング(おまけ)

とりあえず動くが同じアクション内でリクエストにより挙動が変わるのは望ましくない。
また、コントローラーとビューで記述がやや重複しておりDRYではないのでこれも変更。

まずはビューを変更する。パーシャルの中身はjsonで受け取るため不要。

# app/views/books/index.html.erb
<div id="paginator">
</div>
<div id="books">
</div>

ページネーターはajaxで生成するので削除。

$ rm app/views/books/_paginator.html.erb

次にコントローラーのアクションを分ける。

# app/controllers/books_controller.rb
  before_action :set_books, only: [:index, :ajax]

  def index
  end

  def ajax
    # view_contextでpaginateメソッドを使いパーシャルの中身と同じものを生成
    paginator = view_context.paginate(
      @books,
      remote: true,
      url: books_ajax_url # Kaminariのリンク先を変える
    )

    # render_to_stringでパーシャルの中身を生成
    books = render_to_string(
      partial: 'book',
      locals: { books: @books }
    )

    #リクエストを確認しajaxだった場合にはjsonで各パーシャルをクライアントへ返す
    if request.xhr?
      render json: {
        paginator: paginator,
        books: books,
        success: true # クライアント(js)側へsuccessを伝えるために付加
      }
    end
  end

  private
    def set_books
      @books = Book.page(params[:page]).per(10)
    end
end

あとはcoffee(js)に処理を追加する。
最初に表示した段階から読み込みをしなければならず、表示されないと発火トリガーとなるページネーターも生成されないので注意が必要。

// app/assets/javascripts/books.js.coffee
$ ->
  // paginationからajax:successが発火したら処理を開始
  $('#paginator').on 'ajax:success',  (e, result, status, xhr) ->

    // controllerでresultの中に入れたpaginatorとbooksをそれぞれ表示する
    $('#paginator').html result.paginator
    $('#books').html result.books

  // 読み込みした後にajaxでサーバへリクエストを投げる
  $.ajax(
    url: "/books_ajax",  // ajaxアクションを明示
    success: (data, status, xhr) ->  // ajaxの処理が成功した場合
    if data.success
      $("paginator").trigger('ajax:success', [ data, status, xhr])  // "#paginator"の中で"ajax:success"を発火させる
    end
  )

最後のtriggerの部分とかajax:successを渡す部分でかなり躓いた。