Building a real world pagination with Ember.js - the right way

A (not so) short introduction on front end pagination

Every time I face myself with a front end pagination solution out there I often notice that people forget about one important thing, scalability. That is because most of the solutions for front end pagination uses the assumption that the front end is going to be the sole responsible to paginate the data.

If you recall Ember.js headline on their landing page, it says "A framework for creating ambitious web applications", and yes, the ambitious is written in bold. So, if you are building ambitious web applications you are talking about a large amount of data to be handled by your application, and with that you most likely are going to need some sort of pagination.

But to trust the front end as the only responsible for that pagination is to shoot yourself in the foot, for your application will fail to deliver a reasonable user experience when trying to paginate through several thousands of records.

Your application needs to be responsive and try to deliver the best experience for your user, so why are you trying to load all those several thousands of records at once? Specially because most of those records will never be shown to the user, as most likely your user will use some sort of filter/search to find only the records that matters for him at the moment when he access your application.

Backend driven pagination api

For Ember to correctly handle your backend driven pagination, your api must return the pagination data as a meta JSON object side loaded into your main Ember model.

{
  "post": {
    "id": 1,
    "title": "Progressive Enhancement is Dead",
    "comments": ["1", "2"],
    "links": {
      "user": "/people/tomdale"
    },
    // ...
  },

  "meta": {
    "total": 100
  }
}

That will allow Ember to make those attributes to be available to you model through store.metadataFor("post") to correctly manage your pagination status.

Pagination being part of your URL

As any Ember.js core developer would tell you on a presentation, training, keynote, tweet or whatever, the URL bar and the router is the most important part for the Ember.js applications. And I too think that, a well built Ember.js application should be able to rebuild a certain user flow status with a give URL. So, why not include the pagination state in there too?

With that in mind, to make the pagination part of the URL, we are going to make use of a Ember 1.7 feature, queryParams. So without further ado, let's dig into the code.

Implementation for this article

The code demonstrated in this article can be downloaded at https://github.com/WebCloud/ember-generic-paginator. That github project is an ember cli project, although I demonstrate code here without using the ES6 module syntax.


For the sake of simplicity, this article will not be based on top of any backend, as to build a pagination support into your backend is relatively simple to achieve.


First things first, the model

So, to make sure you have something to paginate, let's create a fixtures for your model(s):

Myapp.List = DS.Model.extend({  
  title: DS.attr(),
  description: DS.attr()
});

Myapp.List.reopenClass({  
  FIXTURES: [
    {
      id: 1,
      title: 'My list item...'
    },
    {
      id: 2,
      title: 'My list item...'
    },
    {
      id: 3,
      title: 'My list item...'
    },
    {
      id: 4,
      title: 'My list item...'
    },
    {
      id: 5,
      title: 'My list item...'
    },
    // ...

  ]
});

This is going to ensure that you have some data to paginate.

Implement support for query on FixtureAdapter

For the pagination, and any other kind of filters, to work with the fixture adapter you need to implement the queryFixtures method on the DS.FixtureAdapter (or it's instance with reopen).

Myapp.ApplicationAdapter = DS.FixtureAdapter.extend({  
  queryFixtures: function(fixtures, query){
    var properties;

    properties = Object.keys(query);

    // adding pagination support
    if(properties.contains('offset')){
      fixtures = fixtures.slice(query.offset, query.offset+query.limit);
    }

    return fixtures;
  }
});

To add pagination support is really simple, just verify the presence of the offset parameter and slice the fixture array based on it.

Router: the center piece your app, and the pagination

As previously mentioned, the Router is supposed to be the center piece of your Ember.js application. With that in mind, the most logical approach to make a reusable pagination is to create a Route base object to be used wherever pagination is needed.

You may ask, why not a Mixin? As you would be really fine to create a Mixin for this feature, I would rather use the object inheritance model to implement that.

Mostly because once developed, we can abstract the fetching for the models that needs pagination in your routes, as it is mostly repetitive, and try to be a bit more DRY when it comes to using this feature across your application.

But why do you call it generic?

The reason is simple, one of the development design that I like the most to use, whenever possible, is Generics, and its capabilities to enclosure repetitive logic for different types. Pagination proves itself to be a good match to try to apply this concept. The reason pagination for Ember has to be generic is that you can't tie your router/store logic to one specific model/route.

Thankfully Ember, and it's object inheritance model, provides us with the right tools for the task: the _super() method.

You might have seen this method in your init function calls, or even in your setupController calls within your routes. The reason is that Ember relies heavily on inheritance for it's own internal structure to work.

If you have browsed through Ember.js api documentation, you might have one day stumbled upon the Ember.Object class. That is the main class used across all Ember structure. So, this approach is not at all strange to Ember, and we should take advantage of it whenever necessary.

The PaginationBase route

So without further ado, let's begin to build our core object for this pagination solution. As mentioned before, we will need to somehow make this base object generic.

The way to do it is to set what domain (model) are you going to work with. So let's overwrite the init call to pass our domain class from our instances.

Myapp.PaginationBaseRoute = Ember.Route.extend({  
  // ...
  init: function(domain){
    this._super();
    this.set('domain', domain);
  }
  // ...
});

The call for _super() in our init method is required, and is just to call the default constructor from the Ember.Route object that we are inheriting from. That is the same way our instances are going to do to pass on the domain parameter to be stored.

queryParams and pagination meta data

For our pagination to work correctly, and be re-accessible with a correct URL, let's use queryParams to pass on/read our page object to be used.

Myapp.PaginationBaseRoute = Ember.Route.extend({  
  offset: 0,
  limit: 10,
  queryParams: {
    page: {
      refreshModel: true
    }
  }
  // ...
});

We want to every time that the page number changes, a new batch of data is requested from the API. So for that, we setup our queryParams property, page, with the option refreshModel: true to call the model hook so our model is refreshed accordingly.

Note that we also setup the offset and limit properties and their default values.

Fetching our model data

Let's create our model hook to fetch from the api the part of the data that we want, based on the page object passed through as a query parameter.

Myapp.PaginationBaseRoute = Ember.Route.extend({  
  // ...
  model: function(params){
    var page;

    if(params.page){
      page = params.page;
      // avoid page numbers to be trolled i.e.: page=string, page=-1, page=1.23
      page = isNaN(page) ? 1 : Math.floor(Math.abs(page));
      // page=1 will result into offset 0, page=2 will result into offset 10 and so on
      this.set('offset', (page-1)*this.get('limit'));
    }

    // ...
  }
  // ...
});

First of all, we make sure that the page parameter holds a valid page number with this simple sanitization. Afterwards, we set the offset according with the page passed.

After that, we fetch our model data based on the calculated offset and the default limit value.

Myapp.PaginationBaseRoute = Ember.Route.extend({  
  // ...
  model: function(params){
    // ...
    return this.store.find(this.get('domain'), {
      offset: this.get('offset'),
      limit: this.get('limit')
    });
  }
  // ...
});

Fetching from Fixtures

When fetching from FIXTURES you need to setup manually the total property, by using the store.metaForType method. That property will hold the total amount of records, and it will be used later to control the pagination flow in the controller.

Myapp.PaginationBaseRoute = Ember.Route.extend({  
  // ...
  model: function(params){
    // ...
    // Set's the metaForType manually, on your backend api you would have it
    // like it's documented: http://emberjs.com/guides/models/handling-metadata/
    // once you have the api in place just remove this bit
    this.store.find(this.get('domain')).then(function(that){
      return function(completeList){
        that.store.metaForType(that.get('domain'), {total:completeList.get('length')});
      };
    }(this));
    // ...
  }
  // ...
});

Setup your controller properties

The last thing to do in our paginated route flow is to setup our controller pagination properties accordingly.

Myapp.PaginationBaseRoute = Ember.Route.extend({  
  // ...
  setupController: function(controller, model){
    this._super(controller, model);
    controller.setProperties({
      offset: this.get('offset'),
      limit: this.get('limit')
    });
  }
});

This will make sure that we pass to our controller not only the paginated model, but the info about our pagination to further control, like the pagination component for the template.

Using the route base object

In order to have the pagination feature on whatever route you might want, you just need to implement the base object and pass the domain that the route refers to (user, post, comment, etc).

Myapp.MyRoute = PaginationBaseRoute.extend({  
  init: function(){
    this._super('model');
  },
  // I can also overwrite the default limit of 10 to have more pages
  limit: 5
});

As we already have provided the implementation for the model hook, it will use it by default, unless you want to overwrite to do some extra things.

But I want/have to do more things in my model hook

If you need/want to extend the default behavior from the base route object's model hook you can simply overwrite it. As long as you don't forget to call the _super() function you shall still receive your paginated data accordingly.

Myapp.MyRoute = PaginationBaseRoute.extend({  
  // ...
  model: function(params){
    model = this._super(params);

    // here I will do my extended behaviour
    model.get('firstObject').set('head', true);

    return model
  }
});

Mixin the things

Now it's time to build the glue that will hold the pagination feature together, which is the Paginated mixin. With it we are going to add support for the necessary features to drive the pagination from the controller, like the queryParams property.

Myapp.Paginated = Ember.Mixin.create({  
  queryParams: ['page'],
  page: 1,
  offset: 0,
  // ...
});

The queryParams: ['page'] is going to setup a query parameter page to be set/read to/from our internal variable page. For more info and possible customization of this property visit the Ember.js documentatiion.

Setup pagination control variables

In order to correctly navigate through our paginations we need to verify if the next/previous page number is a valid page number for the data to be paginated, according with the offset and total attributes.

Myapp.Paginated = Ember.Mixin.create({  
  // ...
  hasPreviousPage: function(){
    return this.get('offset') !== 0;
  }.property('offset'),
  hasNextPage: function(){
    return (this.get('offset') + this.get('limit')) < this.get('total');
  }.property('offset', 'limit', 'total'),
  // ...
});

These computed properties will be used in our pagination component to enable/disable it accordingly.

For the page navigation we are going to create two actions, nextPage and previousPage.

Myapp.Paginated = Ember.Mixin.create({  
  // ...
  actions:{
  previousPage: function(){
      // Just a small tweak to the previous button
      // if by any chance the user hits a url that
      // has a page that is higher than the actual total pages (this is only possible manually)
      // as he tries to come back to the previous page
      // he will get the last possible page number
      var totalPages = Math.ceil(this.get('total')/this.get('limit'));
      if(this.decrementProperty('page') > totalPages){
        this.set('page', totalPages);
      }

      this.transitionToRoute({
        queryParams: {
          page: this.get('page')
        }
      });
    },

    // ...
  }
});

For the previous page we do some extra sanity check, in order to avoid reaching for an page that doesn't exists. If the user types a wrong page number, we are going to reset the page number to the last possible page.

Myapp.Paginated = Ember.Mixin.create({  
  // ...
  actions:{
    // ...
  nextPage: function(){
      this.transitionToRoute({
        queryParams: {
          page: this.incrementProperty('page')
        }
      });
    }
  }
});

For the nextPage action there is no extra checking needed to be done, as our hasNextPage property will return false if we enter a higher page number than our total property indicates.

A paginated controller

That Mixin that we just created will be used by any controller that covers the pagination.

Myapp.MyController = Ember.ArrayController.extend(Paginated, {  
  // ...
});

For the mixin to correctly work you need to setup the total computed property, as it will be needed to calculate the page numbers and compute the hasNextPage computed property. The total computed property will be using our meta data that will come either from the API or manually set through the store.metaForType function, in case as we are using the DS.FixtureAdapter.

  total: function(){
    return this.store.metadataFor('list').total;
  }.property('model')

Pagination navigation component

Now lets create the pagination navigation buttons. They are quite simple, we just have to enable/disable based on the hasNextPage/hasPreviousPage and send the action nextPage/previousPage to the controller.

Myapp.PaginatonBaseComponent = Ember.Component.extend({  
  tagName: 'button',
  classNames: 'btn btn-default'.w(),
  attributeBindings: ['disabled'],
  // the enabled property will easily toggle the disabled attribute for the element
  // in case there's no more items to iterate
  enabled: true,
  disabled: Ember.computed.not('enabled'),
  action: null,
  click: function(){
    this.sendAction();
  }
});

The template for that component simply has to {{yield}} whatever you pass to it. For instance, that can be just the text for the button or some HTML to extend the component functionality.

To use the component is simple, you just have to map what action you want to trigger/bubble on/to the controller and pass the text to be displayed.

{{#pagination-base action='previousPage' enabled=hasPreviousPage }}previous{{/pagination-base}}
{{#pagination-base action='nextPage' enabled=hasNextPage }}next{{/pagination-base}}

That's a wrap

pagination preview

The intention with this post and study was to deliver real world pagination example. When you find yourself having to program or scale a big, or should I say ambitious, Ember.js application you will want to find balance between loading resources and managing them. In those situations you might wan to to share the load with the backend and Ember provides just the right tools for that.

Also, I am not saying that this solution is the definitive one, I am only saying this is one that works for real world applications. For instance, you might want to check in your generic route base object if you records list is less than 3 (or any amount that you find reasonable) pages you would want to load all of them already to spare some HTTP requests, and so on.

Onec again the source code for this Ember generic paginator have some more things not covered here, so feel free to browse through and change it as you wish.

If you wish to take this study even further see the links bellow: