Kaminari という gem がありまして、お世話になっています。
<%= paginate @collection, param_name: 'query[page]' %>
したくなったわけです。できないのですよ。なんか上手く動かない。
めっちゃ動きそうじゃん。
<% { query: { page: 1 } }.deep_stringify_keys.deep_merge(Rack::Utils.parse_nested_query("query[page]=2")) %>
動いたんですよ。
masterのコードが使いたい
どうやらこのコード、masterブランチにあるみたいなんです。リリースされてる 0.16.3 のブランチにはなかった。なので、master が安全そうならそっちを使いたい。(stableブランチにもなかった)
安定しているかどうかは悩ましい部分ですが、多分安定してるだろうとは思ったのですね。ただまあ、一応手元の環境でテストくらいはやっておきたい。
作った人にTwitterで聞いてみるのも手かと思ったんですが、人見知りなのでアレなのと、まあその場合もテストくらいやってからでいい。あと「そんなん自分で判断しろよ」って思われそうで嫌だ。(自分だったら思う)
なのでテストを走らせたい。
テストしたい
曰く、bundle exec rake spec:all
で全部テストできるぜ、とのこと。
やってみたけど当然 bundle install
を求められて、しかもgemfilesディレクトリになんかいっぱいいる。これ全部 bundle install
する方法ってあるんだろうか。多分あるんだろうけど、知らなかったので力技で解決する。
find gemfiles -name '*gemfile' | xargs -I{} bundle install --gemfile {}
できた。なんか最新の(Rails 5 系のコードとか?)が ruby 2.2.2+ じゃないと動かないらしいので、実際には
rvm install 2.2.2
find gemfiles -name '*gemfile' | xargs -I{} bundle install --gemfile {}
だった。rbenv は使ってない。
そこまでやる必要なかった
gemfile の違いなんですが、要するに古い環境とか ActiveRecord 以外の ORM を使っている場合のテストを走らせたいって感じなので、なにかコミットしたいならともかく、自前のプロジェクトに組み込むかどうかの大味な判断材料にするために全部の gemfile でテストする必要はないわけですね。愚かだった。
とりあえずテストは全部動作したので使ってみます。
ちなみに
Kaminari のコード読んだ記録も書いときます。ざっくり読んだだけですが。
Kaminari::ActionViewExtension
が ActionView
の拡張です。paginate メソッドが定義されてます。
# A helper that renders the pagination links.
#
# <%= paginate @articles %>
#
# ==== Options
# * <tt>:window</tt> - The "inner window" size (4 by default).
# * <tt>:outer_window</tt> - The "outer window" size (0 by default).
# * <tt>:left</tt> - The "left outer window" size (0 by default).
# * <tt>:right</tt> - The "right outer window" size (0 by default).
# * <tt>:params</tt> - url_for parameters for the links (:controller, :action, etc.)
# * <tt>:param_name</tt> - parameter name for page number in the links (:page by default)
# * <tt>:remote</tt> - Ajax? (false by default)
# * <tt>:ANY_OTHER_VALUES</tt> - Any other hash key & values would be directly passed into each tag as :locals value.
def paginate(scope, options = {}, &block)
options[:total_pages] ||= options[:num_pages] || scope.total_pages
paginator = Kaminari::Helpers::Paginator.new(self, options.reverse_merge(:current_page => scope.current_page, :per_page => scope.limit_value, :remote => false))
paginator.to_s
end
読めば大体わかります。Kaminari::Helpers::Paginator.new の 第一引数にある self ってのは、ActionView そのものですね。読むまで知らなかったんですが、ActionView のオブジェクトは params を持っていて、paginate 内部でこれを継承しています。
例えば /anywhere?params=value
の時に Kaminari で生成されたページネイションのリンクのURLは /anywhere?params=value&page=2
とかになるわけです。このURLの生成時に使っています。
この辺りの処理の主役は Kaminari::Helpers::Tag
クラスです。
# A tag stands for an HTML tag inside the paginator.
# Basically, a tag has its own partial template file, so every tag can be
# rendered into String using its partial template.
#
# The template file should be placed in your app/views/kaminari/ directory
# with underscored class name (besides the "Tag" class. Tag is an abstract
# class, so _tag partial is not needed).
# e.g.) PrevLink -> app/views/kaminari/_prev_link.html.erb
#
# When no matching template were found in your app, the engine's pre
# installed template will be used.
# e.g.) Paginator -> $GEM_HOME/kaminari-x.x.x/app/views/kaminari/_paginator.html.erb
class Tag
def initialize(template, options = {}) #:nodoc:
@template, @options = template, options.dup
@param_name = @options.delete(:param_name) || Kaminari.config.param_name
@theme = @options.delete(:theme)
@views_prefix = @options.delete(:views_prefix)
@params = template.params.except(*PARAM_KEY_BLACKLIST).merge(@options.delete(:params) || {})
# @params in Rails 5 does no more inherits from Hash but composes a Hash
if @params.instance_variable_defined?(:@parameters) && !@params.respond_to?(:deep_merge)
@params = @params.instance_variable_get :@parameters
else
@params = @params.with_indifferent_access
end
end
まあこんなコピペ読むなら元コード読んだほうが良いです。
で、まあ先ほどの paginate
メソッドとこのクラスの間にはもう1クッションあったんですが、とりあえず割愛します。template がだいたい ActionView になります。したがって @params には GET パラメータが入るわけです。
生成されるページネイションの URL はいろいろあって page_url_for
メソッドが担当しています。この辺はまあテストを読んだらそう書いてました。呼び出しの手順とかは読んでないのでわからないですが、ともかく URL の生成処理はここです。
def params_for(page)
page_params = Rack::Utils.parse_nested_query("#{@param_name}=#{page}")
page_params = @params.deep_merge(page_params)
ここでアレしてるわけですね。deep_merge
で、page_params の内容で @params を上書きしてる感じです。deep なのは再帰的にやるってことです。多分。
これで <%= paginate @collection, param_name: 'query[page]' %>
とかも動くわけですね。
0.16.3の場合
まず Kaminari::Helpers::Tag
の @params から。
@params = @options[:params] ? template.params.merge(@options.delete :params) : template.params
あー、ブラックリストがないんですね。
次に page_url_for
def page_url_for(page)
@template.url_for @params.merge(@param_name => (page <= 1 ? nil : page), :only_path => true)
end
merge なんですね。@param_name にはオプションの params_name: ?? が入ってるので、query[page]
が入ってることになります。一方 @params は { query: { page: 1 } }
みたいな感じなので、 merge してもうまくいかないっぽい感じがしますね。
細かく言えばバージョンとか必要なんでしょうけど、とりあえずやってみた結果。
[1] pry(main)> { query: { page: 1 } }.merge('query[page]' => 2)
=> {:query=>{:page=>1}, "query[page]"=>2}
そうですね。
ここから先読み進めたわけではないのですが、多分もともと param_name: query[page]
みたいな使い方は想定していなかったんだと思います。
describe '#param_name' do
before do
@paginator = Paginator.new(template, :param_name => :pagina)
end
subject { @paginator.page_tag(template).instance_variable_get('@param_name') }
it { should == :pagina }
end
0.16.3 のテストで param_name のテストしてるのはここくらいな気がする。(全部精読したわけではないです)
master だと増えていて、たとえば
context "with param_name = 'user[page]' option" do
before do
helper.params.merge!(:user => {:page => "3", :scope => "active"})
end
context "for first page" do
subject { Tag.new(helper, :param_name => "user[page]").page_url_for(1) }
if ActiveSupport::VERSION::STRING < "3.1.0"
it { should_not match(/user\[page\]=\d+/) }
it { should match(/user\[scope\]=active/) }
else
it { should_not match(/user%5Bpage%5D=\d+/) } # not match user[page]=\d+
it { should match(/user%5Bscope%5D=active/) } # match user[scope]=active
end
end
context "for other page" do
subject { Tag.new(helper, :param_name => "user[page]").page_url_for(2) }
if ActiveSupport::VERSION::STRING < "3.1.0"
it { should match(/user\[page\]=2/) }
it { should match(/user\[scope\]=active/) }
else
it { should match(/user%5Bpage%5D=2/) } # match user[page]=2
it { should match(/user%5Bscope%5D=active/) } # match user[scope]=active
end
end
end
みたいなテストがされています。やったぜ!
余談
- gemfiles の中身を全部突っ込む冴えたやり方ってなんかあるんですかね。
- 現行の master がリリースされることを願っています。手伝う? いや英語わかんないから……。
-
gem 'kaminari', ref: '45ee71b74c7a21333e2b05ae8672a0e8c0625e38', github: 'amatsuda/kaminari'
しました。 - master 使うのがアレだったら fork してパッチ当てたやつを使うのが正しい作法なのかな? 手元の lib とかにパッチ書いて include させるのも良いのかもしれない。あんま好きなやり方ではない。