Query Params, Metadata and Pagination in Ember.js

A little earlier this year, I was reading an organisation’s style guide for working with Ember.js, when I encountered reference to the use of ‘query params’ in a controller. At the time, still relatively new to Ember, I had no idea what this meant. In the intervening time, though I’ve learned a lot. Basically, query params allow for certain application states to be serialised into the URL that wouldn’t be otherwise possible through the regular use of routes. In this blog post, I’m going to explain the role of query params, and how they can be used in your Ember app to provide some neat functionality. I’ll be using a sample Ember.js front-end app, with a Rails API on the back-end.

Query Param Basics

Consider the following URL: http://www.fakewebsite.com/recipes?page=2. The text to the right of the ‘?’ is a query param - in this case ‘page’, with a value of 2. As you might guess, this url represents the second ‘page’ of a longer list of recipes. If you had 1000 recipes in your database, you can understand the utility for paginating the index page into smaller chunks. The user probably finds it easier to read a smaller number of recipes, from a UX perspective it may be preferable to avoid scrolling through long lists, and if there is any heavy logic being done on the back-end, it’s faster to simply request 10, 25 or 50 entries at a time than the full 1000. Other uses for query params can include sorting and filtering your database according to certain properties.

Query params are declared in the relevant controller of the route you’re working with. So, if you want query params for a ‘recipes’ route, the query params are declared in app/controllers/recipes.js (or the controller.js file in the recipes directory if you’re working with pods). Specific instructions for declaring query params can be found in the Ember docs, but lets build out an app to see how they work.

Getting started

To test out query params, I built an Ember.js app that displays a list of friends with a number of properties (like name, email, phone number, age, etc). The data is served as JSON from a Rails API.

The finished code can be found here: Ember front-end & Rails API

Setting up the Rails API and adding a root node

I’m not going to go into all the steps of setting up a Rails API. Sophie DeBenedetto has a great step-by-step guide for this on her blog here. This app has a single model, Person, and a single route people. You can take a look at the migration file to see the properties of the model.

To populate the database with entries, I wrote a seed.rb file that makes 400 instances of people using the Faker gem which automatically generates content for different types of properties, like this:

1
2
3
4
5
6
7
8
9
10
11
400.times do
  person = Person.create(
    first_name: Faker::Name.first_name,
    last_name: Faker::Name.last_name,
    nick_name: Faker::Superhero.name,
    age: Faker::Number.between(18, 99),
    email: Faker::Internet.email,
    phone: Faker::PhoneNumber.phone_number,
    friends_since: Faker::Time.backward(5004, :evening)
  )
end

There is one extra thing we need to do beyond Sophie’s instructions: recent updates to the Active Model Serializer gem use the Attributes adapter for serializing JSON, which excludes the root node (eg, the name of the model). As such, we get JSON like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
id: 1,
first_name: "Aiden",
last_name: "Stamm",
nick_name: "Doctor Match",
age: 35,
email: "devan@predovicortiz.org",
phone: "562.135.0643 x8216",
friends_since: "2007-01-09T00:49:32.000Z"
},
{
id: 2,
first_name: "Jamie",
last_name: "Boyer",
nick_name: "Songbird",
age: 56,
email: "neva@hartmann.co",
phone: "(858) 284-7728",
friends_since: "2007-07-30T21:08:20.000Z"
}

For the model hook in Ember to function properly, we need a root node with the name of model. To do so, we need to instruct Rails to use the JSON adapter. In the config/intializers directory, I included a serializer.rb file with the following line of code:

1
ActiveModelSerializers.config.adapter = :json

With that change, the JSON served by our API looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
people: [
{
id: 1,
first_name: "Aiden",
last_name: "Stamm",
nick_name: "Doctor Match",
age: 35,
email: "devan@predovicortiz.org",
phone: "562.135.0643 x8216",
friends_since: "2007-01-09T00:49:32.000Z"
},
{
id: 2,
first_name: "Jamie",
last_name: "Boyer",
nick_name: "Songbird",
age: 56,
email: "neva@hartmann.co",
phone: "(858) 284-7728",
friends_since: "2007-07-30T21:08:20.000Z"
}
]

That’s great for now - our API is serving JSON. Let’s set up our Ember front-end.

Building an Ember app and connecting it to the API

To start, generate a new Ember app with the ember new command (for more, see the Ember docs).

In this project, I just wanted to build an index page for all the people in the database. As such, we need to set up the route, model, route handler, template and controller for ‘people’ (fun note: the Inflector in Rails is smart enough to know that ‘people’ is the plural of ‘person’). Run ember g resource people. This creates three important files: app/models/person.js, app/routes/people.js, and app/templates/people.hbs (as well as a file for tests). It also adds the appropriate route - if you look in your app/router.js file you’ll see the following:

1
2
3
4
5
6
7
8
9
10
11
12
import Ember from 'ember';
import config from './config/environment';

const Router = Ember.Router.extend({
  location: config.locationType
});

Router.map(function() {
  this.route('people');
});

export default Router;

If we run our server (ember s) we can navigate to http://localhost:4200/people. There’s nothing there yet, of course, but it’s a route! We need to fill out a few more things before our Ember app can actually make requests to our Rails API. First, lets build out the rest of the ‘people’ resource.

We need to define the ‘person’ model and the types of properties that will provided by our API. In app\models\person.js we add the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
import DS from 'ember-data';
import attr from 'ember-data/attr';

export default DS.Model.extend({
  first_name: attr(),
  last_name: attr(),
  nick_name: attr(),
  age: attr(),
  email: attr(),
  phone: attr(),
  friends_since: attr(),

});

We then need to add a model hook to the ‘people’ route handler that will load the model from the store. In the app\routes\people.js we add the following:

1
2
3
4
5
6
7
import Ember from 'ember';

export default Ember.Route.extend({
  model: function() {
    return this.store.findAll('person');
  }
});

For the moment, we don’t need to add anything new to our controller, but we do need to fill out the app/templates/people.hbs template a bit. To make our front-end styling a little simpler, I’m using the Zurb Foundation responsive framework. Don’t worry if you don’t understand how it works. I installed into my Ember app using the Ember Cli Foundation Sass addon.

In the template, I build a table of all the people in our database using the #each model as helper. It looks like this (ignore the spaces between the curly braces):

{ {#each model as |person|} } {{/each}}
First Name Last Name Nickname Age Email Phone Friends Since
{ {person.first_name} } { {person.last_name} } { {person.nick_name} } { {person.age} } { {person.email} } { {person.phone} } { {person.friends_since} }

If we navigate to http://localhost:4200/people we can see our table rendered with the people in our database! That’s great. Though, with 400 entries in the database, it involves a LOT of scrolling:

Ideally, we’d like to see things in smaller chunks - that’s the whole point of this blog post, afterall! To do this, we need to go back to our API and include some metadata about pagination in the JSON being served.

Paginating JSON and serving metadata

When a GET request is sent to the api/v1/people route of our API, the back-end is currently serving all 400 entries back as a response. We need a way to both break the JSON being served into smaller segments, but also to serve information about the data itself (metadata). In the case of pagination, this metadata will include information like how many chunks or ‘pages’ the database has been segmented into, as well as the current page being served.

The simplest way to incorpate pagination into your API is through the Kaminari gem. There are other options out there, but the comments I’ve seen on Stack Overflow and elsewhere seem to favour this gem. Install it by including gem 'kaminari' in your Gemfile, and running $ bundle. With this done, we need to make some changes to our People Controller, as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Api::V1::PeopleController < ApplicationController

  def index

    info = {
      page: params[:page] || 1,
      per_page: params[:per_page] || 10
    }

    @people = Person.page(info[:page]).per(info[:per_page])

    render json: @people, params: info
  end

end

The info hash sets defaults for the current page, as well as the number of entries per page. Eventually, params included in requests from the Ember app will include the page number, which is what is being used when the Person model is being queried to create the @people variable being rendered as JSON. If we go to http://localhost:3000/people, we’ll see that only 10 entries are being served in the JSON. If we got to http://localhost:3000/api/v1/people?page=2 we see the next 10 in the database. Cool!

But that’s not all - we also need to serve the metadata, telling the Ember app things like which page we’re on and how many pages there currently are. Looking at the documentation for Active Model Serializer, the solution is quite simple. In our API controller (effectively the application controller), we add the following:

1
2
3
4
5
6
7
8
9
def pagination_dict(object)
  {
    current_page: object.current_page,
    next_page: object.next_page,
    prev_page: object.prev_page, # use object.previous_page when using will_paginate
    total_pages: object.total_pages,
    total_count: object.total_count
  }
end

And in our People Controller, we make a small addition to the code rendering our JSON:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Api::V1::PeopleController < ApplicationController

  def index

    info = {
      page: params[:page] || 1,
      per_page: params[:per_page] || 10
    }

    @people = Person.page(info[:page]).per(info[:per_page])

    render json: @people, params: info, meta: pagination_dict(@people)
  end
end

Our JSON now has a ‘meta’ key attached to the end, like this:

1
2
3
4
5
6
7
meta: {
current_page: 1,
next_page: 2,
prev_page: null,
total_pages: 40,
total_count: 400
}

We’ll be able to execute a query for this in the People route handler in Ember, giving us access to this metadata in the view. So, now it’s time to head back to the Ember app!

Handling metadata and pagination in Ember

We need to do a number of steps to handle and use the metadata being served with our JSON in Ember. First, we go to the route handler and change the code to the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import Ember from 'ember';

export default Ember.Route.extend({

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

  model: function(params){
    return this.store.query('person', params);
  }

});

We’ve done a couple of things here. The queryParams property includes a key called ‘page’. By setting the refreshModel property to ‘true’, any changes to the page query param will fire the model hook and make a new request to the API. So if the url is changed from http://localhost:4200/people to http://localhost:4200/people?page=2, the model will be updated automatically.

Next, we need to actually declare our ‘page’ query param in the app/controllers/people.js file, and give it some logic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import Ember from 'ember';

export default Ember.Controller.extend({
  queryParams: ['page'],
  page: 1,

  metaData: Ember.computed('model', function(){
    let meta = this.get('model.meta');
    meta['second_last'] = meta.total_pages - 1;
    meta['third_last'] = meta.total_pages - 2;
    return meta;
  }),

  lastThreePages: Ember.computed('model', function(){
    if (this.get('page') < this.get('metaData.total_pages') - 3) {
      return false;
    } else {
      return true;
    }
  }),

  actions: {
    nextPage() {
      if (this.get('page') < this.get('metaData.total_pages')){
        let page = this.get('page');
        this.set('page', page + 1);
      }
    },

    prevPage() {
      if (this.get('page') > 1) {
        this.set('page', this.get('page') - 1);
      }
    }
  }

});

So what’s going on here? First, the queryParams array is defining which specific query params we’ll be using. In this case, just one: ‘page’. The ‘page’ property below it gives it a default value of 1 - so if we navigate to just http://localhost:4200/people, it will assume we want the first page.

In the metaData computed property, we are accessing the information in the ‘meta’ key in the JSON our API is serving, giving us access in the view to information like the current page, which we can invoke with metaData.current_page.

There are also a pair of actions: nextPage and prevPage. These do pretty much what you’re thinking they will: when triggered, it will change the ‘page’ query param to the next or previous page, thus updating the model and serving new entries from the database in our view. In the template we’ll have two buttons that trigger these actions. We use conditional statements to make sure we can’t advance past the last page in the database, or to a page number lower than 1.

Don’t worry about the ‘lastThreePages’ computed property for now - I’m adding the pagination system available in Zurb Foundation, and we’ll use that property for some conditionals in the view to change how certain buttons are rendered depending on which page we’re on.

So, let’s add that to our app/templates/people.hbs files and see what we get. In the template, below the original code for the table, we add new code for pagination buttons. Normally we might choose to separate this into a component, but for time’s sake I’ve just included it in the template itself. The code is a bit lengthy and not particularly important to what this blog is about. If you’d like to see it, it’s in the repository here, but I am also certain there are more elegant ways of doing it.

With that all in place, we get something that looks like this:

As the buttons on the bottom are pressed, the ‘nextPage’ function is triggered, updating the ‘page’ query param and making a new request to our Rails API (again, because we set the refreshModel property to ‘true’). The person controller in our API takes in the page parameter in the request, queries the database and serves the appropriate data and metadata back to our Ember app. The controller takes the data for the model, extracts the metadata, and then passes it to update the view.

Conclusions

So that’s a first look at query params, metadata and pagination in Ember! Don’t forget, we can use queryParams to serialise a number of different application states into our URL beyond mere pages. We have a filter query param that looks at a particular property and only returns instances from the database where a condition is met. In this example, maybe we filter to find friends who are a particular age. Or we could sort according to certain categories - maybe I’d like to see my friends ranked according to how long I’ve known them. We can accomplish all this and more with query params.