【フロントエンド学習記録】Rails と Ember.js でページネーションを実装

こちらのブログ記事の続きです。 今回はRails と Ember.js でページネーションを実装していきます。

maimux2x.hatenablog.com

kaminari を利用する

Rails でページネーションを実装する場合、 kaminari や pagy などの gem がありますが、今回は kaminari を使いました。

github.com

github.com

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 を使っています。

github.com

以下のようにして 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 を利用してページネーションを表示するようにしていきます。

getbootstrap.jp

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">&raquo;</span>
    </LinkTo>
  {{else}}
    <span class="page-link" aria-hidden="true">&raquo;</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 で最後のページまでの連番を生成しています。
  • currentPagination コンポーネント@current で渡される情報
    • 最初のページか最後のページ下で前後のページを計算するかページが存在しないかを制御する

developer.mozilla.org

ここで少しハマったのが 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 できるようにします。

developer.mozilla.org

developer.mozilla.org

ただ、これだけでも実はまだ実装が足りず、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 を書くことにも少し慣れてきたような?