こちらのブログ記事の続きです。 今回はRails と Ember.js でページネーションを実装していきます。
kaminari を利用する
Rails でページネーションを実装する場合、 kaminari や pagy などの gem がありますが、今回は kaminari を使いました。
kaminari の導入方法については省略します。
Rails だけで View も実装している場合はページネーションの導入はスムーズに対応しやすいですが、フロントエンドが別のフレームワークだと Rails のみの場合よりも考える点が増えます。
まず、ページネーションをつけたい画面が全何ページになるのかという情報をフロントエンド側は知る必要があります。
kaminari にはページの情報を取得するメソッドが多数用意されていて、total_pages
メソッドを使うとよさそうです。
# kaminari の README より User.count #=> 1000 User.page(1).limit_value #=> 20 User.page(1).total_pages #=> 50 User.page(1).current_page #=> 1 User.page(1).next_page #=> 2 User.page(2).prev_page #=> 1 User.page(1).first_page? #=> true User.page(50).last_page? #=> true User.page(100).out_of_range? #=> true
フロントで使用している Ember.js は JSON を受け取って画面を描画するため、Rails 側では JSON を返すための API を用意しています。
JSON を構築するために jb という gem を使っています。
以下のようにして Ember.js で 全ページ数の情報を受け取れるようにします。
# app/controllers/posts_controller.rb def index @posts = Post.order(created_at: :desc).page(params[:page]) end
# app/views/posts/index.json.jb { posts: render(partial: "post", collection: @posts), total_pages: @posts.total_pages }
フロント側は Bootstrap を利用してページネーションを表示するようにしていきます。
Bootstrap のページネーションコンポーネントを使用して、Ember.jsでもコンポーネントとしてページネーションを使いまわせるようにしていきます。
以下はその一部です。
// web/app/components/pagination.hbs <li class="page-item {{unless this.next 'disabled'}}"> {{#if this.next}} <LinkTo @route={{@route}} @query={{hash page=@last}} class="page-link" aria-label="Next"> <span aria-hidden="true">»</span> </LinkTo> {{else}} <span class="page-link" aria-hidden="true">»</span> {{/if}} </li>
{{unless this.next 'disabled'}}
は最後のページの場合を制御している- Ember.js が用意している
LinkTo
コンポーネントにルートとしてURLの情報とパラメータとしてページ番号を付与している
よく見るこんな感じのページネーションです。
コンポーネントは以下のようにテンプレート側で使用します。 ここで Rails から受け取った全ページ数の情報を最後のページ番号として利用しています。
// web/app/templates/admin/posts/index.hbs <Pagination @route="admin.posts" @current={{this.page}} @last={{@model.total_pages}} />
続いてコンポーネントとセットになる JavaScript を用意していきます。
// web/app/components/pagination.js import Component from '@glimmer/component'; export default class PaginationComponent extends Component { get pages() { return Array.from({ length: this.args.last }, (v, i) => i + 1); } get prev() { const { current } = this.args; return current === 1 ? undefined : current - 1; } get next() { const { current, last } = this.args; return current === last ? undefined : current + 1; } }
Array.from
で最後のページまでの連番を生成しています。current
はPagination
コンポーネントの@current
で渡される情報- 最初のページか最後のページ下で前後のページを計算するかページが存在しないかを制御する
ここで少しハマったのが this.args
ではページ数の情報にアクセスできますが、データ型がデフォルトだと string
なため integer
として扱えるようにする必要があります。
Ember.js のテンプレートと対になるコントローラーでデータ型を制御する必要がありました。
// web/app/controllers/index.js export default class IndexController extends Controller { queryParams = [ { page: { type: 'number' }, }, ]; @tracked page = 1; }
また、@tracked
でページ番号の変更が Ember.js 側で認識されるようにすることで、ページ番号をクリックする際 <<
, <
記号部分のdsiabled
の制御が適切に実行されるようになります。
ここまででページネーションの表示自体と URL へパラメータとしてページ番号を付与する部分は完了です。 ですが、このままだとページ番号を押しても動きません。
パラメータとともに Rails 側へリクエストを送る必要があるため、その部分を実装していきます。
// web/app/routes/admin/posts/index.js import Route from '@ember/routing/route'; export default class IndexRoute extends Route { async model(args) { const url = new URL('http://localhost:3000/posts'); url.searchParams.set('page', args.page); return await fetch(url).then((res) => res.json()); } }
URLsearchParams.set()
メソッドでパラメータを付与して fetch
できるようにします。
ただ、これだけでも実はまだ実装が足りず、LinkTo
コンポーネントを使ってパラメータを変化させてページを再描画させたい場合は以下を追加する必要があります。
// web/app/routes/admin/posts/index.js import Route from '@ember/routing/route'; export default class IndexRoute extends Route { // refreshModel を追記 queryParams = { page: { refreshModel: true, }, }; async model(args) { const url = new URL('http://localhost:3000/posts'); url.searchParams.set('page', args.page); return await fetch(url).then((res) => res.json()); } }
これでページネーションが動作するようになります。
まとめ
Rails のみでページネーションを導入する際は gem を入れて、その gem が提供する API を利用することで、あまり深く考えずにページネーションを実装することができていたことに気づかされました。
今回はこの部分に気がつくことができたのが一番の学びかなと思います。
JavScript を書くことにも少し慣れてきたような?