Pagination in Ember with a JSON API Backend

Last reviewed on August 7, 2019 with Ember Octane

Setting up pagination with a JSON:API backend is not as straightforward as it should. What is the recommended way to handle this with Ember Data?

How do we deal with query parameters? And the UI?

diagram

A glance at the spec

First step: check what the JSON:API specification has to say about pagination. Basically:

Pagination links MUST appear in the links object that corresponds to a collection. To paginate the primary data, supply pagination links in the top-level links object.

[…]

The following keys MUST be used for pagination links:

  • first: the first page of data
  • last: the last page of data
  • prev: the previous page of data
  • next: the next page of data

There is also an example we can refer to. It’s simple. Given a query to /articles?page[number]=3&page[size]=1, the response shall be:

{
  "data": [
    {
      "type": "articles",
      "id": "3",
      "attributes": {
        "title": "JSON API paints my bikeshed!",
        "body": "The shortest article. Ever."
      }
    }
  ],
  "links": {
    "self": "http://example.com/articles?page[number]=3&page[size]=1",
    "first": "http://example.com/articles?page[number]=1&page[size]=1",
    "prev": "http://example.com/articles?page[number]=2&page[size]=1",
    "next": "http://example.com/articles?page[number]=4&page[size]=1",
    "last": "http://example.com/articles?page[number]=13&page[size]=1"
  }
}

Crystal clear.

Consuming the paginated API

Let’s assume we have a working backend.

If you don’t have a compliant backend, here is a quick Rails 5 beta example with Kaminari.

$ gem install rails
$ rails new articles-api --api

Generate a scaffold for articles

$ cd articles-api
$ bin/rails generate scaffold article title:string body:string

Create seed

# db/seeds.rb

Article.create([
  { title: "Article 1", body: "Lorem 1 ipsum" },
  { title: "Article 2", body: "Lorem 2 ipsum" },
  { title: "Article 3", body: "Lorem 3 ipsum" },
  { title: "Article 4", body: "Lorem 4 ipsum" },
  { title: "Article 5", body: "Lorem 5 ipsum" },
  { title: "Article 6", body: "Lorem 6 ipsum" },
  { title: "Article 7", body: "Lorem 7 ipsum" },
  { title: "Article 8", body: "Lorem 8 ipsum" },
  { title: "Article 9", body: "Lorem 9 ipsum" },
  { title: "Article 10", body: "Lorem 10 ipsum" },
  { title: "Article 11", body: "Lorem 11 ipsum" },
  { title: "Article 12", body: "Lorem 12 ipsum" },
  { title: "Article 13", body: "Lorem 13 ipsum" }
])

Migrate and seed

$ bin/rails db:migrate
$ bin/rails db:seed

Now we have a functional Rails API with data! Next step is to format the data in JSON API and support pagination:

# add gems
$ echo 'gem "active_model_serializers' >> Gemfile
$ echo 'gem "kaminari"' >> Gemfile
$ bundle

# configure AMS for JSON API
$ echo "ActiveModel::Serializer.config.adapter = ActiveModel::Serializer::Adapter::JsonApi" > config/initializers/json_api.rb

# generate article serializer
$ bin/rails g serializer article
# AND EDIT to include attributes :id, :title, :body

In the controller, make sure to paginate if the parameters request so.

# app/controllers/articles_controller.rb

class ArticlesController < ApplicationController
  before_action :set_article, only: [:show, :update, :destroy]

  # GET /articles
  def index
    if params[:page]
      @articles = Article.page(params[:page][:number]).per(params[:page][:size])
    else
      @articles = Article.all
    end

    render json: @articles
  end
# ...
end

Boot the app (binding to all interfaces will ensure Ember CLI proxy can connect to the API)

$ bin/rails s --binding 0.0.0.0

And query the exact URL used in the JSON API reference example to verify it responds correctly:

$ curl "http://localhost:3000/articles?page%5Bnumber%5D=3&page%5Bsize%5D=1"
# {"data":[{"id":"3","type":"articles","attributes":{"title":"Article 3","body":"Lorem 3 ipsum"}}],"links":{"self":"http://localhost:3000/articles?page%5Bnumber%5D=3\u0026page%5Bsize%5D=1","first":"http://localhost:3000/articles?page%5Bnumber%5D=1\u0026page%5Bsize%5D=1","prev":"http://localhost:3000/articles?page%5Bnumber%5D=2\u0026page%5Bsize%5D=1","next":"http://localhost:3000/articles?page%5Bnumber%5D=4\u0026page%5Bsize%5D=1","last":"http://localhost:3000/articles?page%5Bnumber%5D=13\u0026page%5Bsize%5D=1"}}

Success!

Setting up our Ember app

Our app will display that list of articles from the backend:

$ ember new articles -b @ember/octane-app-blueprint --no-welcome
$ cd articles
$ ember generate model article title:string body:string
$ ember g route articles
$ ember g controller articles

We’ll proceed to define the needed query parameters in the controller. Which parameters should we use?

Remember the spec mentioned an URL like /articles?page[number]=3&page[size]=1

Problem! Ember query parameters aren’t capable of supporting query objects or hash-like structures. It’s tempting to want to define page: { number: 3, size: 1 } but… nope.

Dang! Is Ember really ready to work with JSON API?

We are dealing with two different sets of query parameters here:

  • The browser URL query params
  • The backend API query params

We most definitely can supply hash-like structures to our backend with Ember Data.

For our browser URL concern we’ll settle with page and size which correspond to backend URL page[number] and page[size], respectively.

We’ll only use page in our URLs for now, though.

// app/controllers/articles.js

import Controller from '@ember/controller';

export default class ArticlesController extends Controller {
  queryParams = ['page', 'size'];
  page = 1;
  size = 5;
}

Moving on to our route…

// app/routes/articles.js

import Route from '@ember/routing/route';

export default class ArticlesRoute extends Route {

  queryParams = {
    page: {
      refreshModel: true
    },
    size: {
      refreshModel: true
    }
  }

  model(params) {
    return this.store.query('article', { page: {
        number: params.page,
        size: params.size
      }
    });
  }

}

See what we did there?

We mapped our browser URL query params to our backend URL query params.

Oh and by the way, Ember by default does not call model when query params are changed. We tell it to do so any time page/size changes, through refreshModel: true.

So far so good.

Octane news & best practices, straight to your inbox?

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)

We now modify the template to display articles:

{{! app/templates/articles.hbs }}

<ul>
  {{#each @model as |article|}}
    <li>
      {{article.title}}
      <blockquote>{{ article.body}}</blockquote>
    </li>
  {{/each}}
</ul>

Is your backend running? Let’s start our frontend app and check:

$ ember server --proxy=http://localhost:3000

And this is what we (should!) see:

diagram

It works! But 5 articles only?

Our controller reveals exactly why ;)

Implementing client-side pagination

And now? How do we get all those links to, you know, browse through pages and all that?

Let’s go step by step.

We want to have controls for jumping to the previous and next page, our template will have to know which page is previous and which is next.

Our API responded with a links property, remember? That is exactly what we need – but how does the template access those links?

The Bad News is: it can’t.

Shocking – I know. Ember Data models still cannot access the links properties our backend outputs.

The Good News though is: we can adjust a few knobs and make pagination data accessible to our template! Every Ember Data model has a property called meta where arbitrary metadata can be stored.

First approach: Make the backend return links in a JSON API meta field (for example, with Rails we’d use rails-api-pagination). This is a pretty good solution IF changing the server API is an option.

But hey, we are hardcore Embereños. So bring it on baby, we’ll handle it client-side!

We already know how well serializers can massage our data so we will use our prowess to override a serializer.

$ ember g serializer application

As our route model hook calls this.store.query(), then normalizeQueryResponse is the place to deal with responses from that operation. All we want to do is make links accessible in our model’s metadata:

// app/serializers/application.js

import DS from 'ember-data';

export default class ApplicationSerializer extends DS.JSONAPISerializer {

  normalizeQueryResponse(store, clazz, payload) {
    const result = super.normalizeQueryResponse(...arguments);
    result.meta = result.meta || {};

    if (payload.links) {
      result.meta.pagination = this.createPageMeta(payload.links);
    }

    return result;
  }

}

The code says it all: meta.pagination will now hold the links which will be available in the template.

Here is a sample createPageMeta implementation:

// app/serializers/application.js

import DS from 'ember-data';

export default class ApplicationSerializer extends DS.JSONAPISerializer {

  normalizeQueryResponse(store, clazz, payload) {
    // ...
  }

  createPageMeta(data) {

    let meta = {};

    Object.keys(data).forEach(type => {
      const link = data[type];
      meta[type] = {};
      let a = document.createElement('a');
      a.href = link;

      a.search.slice(1).split('&').forEach(pairs => {
        const [param, value] = pairs.split('=');

        if (param == 'page%5Bnumber%5D') {
          meta[type].number = parseInt(value);
        }
        if (param == 'page%5Bsize%5D') {
          meta[type].size = parseInt(value);
        }

      });
      a = null;
    });

    return meta;

  }

}

Adding a simple line to our template will reveal if this is viable:

{{! app/templates/articles.hbs }}

<h1>Last page: {{ @model.meta.pagination.last.number }}!</h1>

<ul>
  {{#each @model as |article|}}
    <li>
      {{article.title}}
      <blockquote>{{ article.body}}</blockquote>
    </li>
  {{/each}}
</ul>
diagram

That is correct!

Let’s put these links to good use and make our app functional. This is a very primitive implementation but should get us a head start:

{{! app/templates/articles.hbs }}

<h1>Current page: {{ @model.meta.pagination.self.number }}</h1>

<ul>
  {{#each @model as |article|}}
    <li>
      {{article.title}}
      <blockquote>{{ article.body}}</blockquote>
    </li>
  {{/each}}
</ul>

{{#each-in @model.meta.pagination as |key value|}}
  <LinkTo @route="articles" @query={{hash page=value.number}}>{{key}}</LinkTo>
{{/each-in}}

Which renders:

diagram

Notice the cool little purple links down there! Go mess around with ‘em.

This is as basic as the previous snippet.

{{! app/templates/articles.hbs }}

<h1>Current page: {{ model.meta.pagination.self.number }}</h1>

<ul>
  {{#each @model as |article|}}
    <li>
      {{article.title}}
      <blockquote>{{ article.body}}</blockquote>
    </li>
  {{/each}}
</ul>

{{#each-in @model.meta.pagination as |key value|}}
  <LinkTo @route="articles" @query={{hash page=value.number}}>{{key}}</LinkTo>
{{/each-in}}

<br>

{{#each this.count as |number|}}
  {{#if (eq number @model.meta.pagination.self.number) }}
    {{ number }}
  {{else}}
    <LinkTo @route="articles" @query={{hash page=number}}>{{number}}</LinkTo>
  {{/if}}
{{/each}}

The eq helper will require the mega-hyper-handy Ember Truth Helpers.

And… what would count be? As we saw earlier, templates are backed by controllers. The count property simply is a generated array of page numbers:

// app/controllers/articles.js

import Controller from '@ember/controller';
import { computed } from '@ember/object';

export default class ArticlesController extends Controller {
  queryParams = ['page', 'size'];
  page = 1;
  size = 5;

  @computed('model.meta.pagination.last.number', 'model.meta.pagination.self.number')
  get count() {
    const total = this.model.meta.pagination.last.number || this.model.meta.pagination.self.number;
    if (!total) return [];
    return new Array(total+1).join('x').split('').map((e,i) => i+1);
  }

}

Result:

diagram

That concludes our JSON API pagination chapter! I hope things make more sense now. Two exercises left for the reader:

  • Styling!
  • Change the pagination size using the size query parameter

Did it go well?

If you are looking for something like a “load more”, try ember-infinity or have a look at Building a UI Around an Ember Data App, specifically Showing all books via metadata.

I couldn’t make Ember CLI Pagination work with this set up. Were you more successful than me perhaps?

Let me know all of this in the comments below!

Enjoyed this article? Join Snacks!

Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)