Last reviewed in August 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?
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 datalast
: the last page of dataprev
: the previous page of datanext
: the next page of dataThere 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.
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)
And query the exact URL used in the JSON API reference example to verify it responds correctly:
Success!
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:
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.
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:
It works! But 5 articles only?
Our controller reveals exactly why ;)
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>
That is correct!
links
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:
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:
That concludes our JSON API pagination chapter! I hope things make more sense now. Two exercises left for the reader:
size
query parameterDid 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!
Snacks is the best of Ember Octane in a highly digestible monthly newsletter. (No spam. EVER.)