In the last episode we implemented a contact card component that was different depending on the type of contact that it was displaying. Today we are going to implement pagination on the frontend. We are going to do our best to map this to the JSON API spec..

In your contacts controller add query params just like we have done before:

app/controllers/contacts.js:

import Ember from 'ember';

export default Ember.Controller.extend({
  queryParams: ['page', 'size'],
  page: 1,
  size: 10
});

In our route, we can specify the behavior of queryParams as they relate to refreshing the model. By default when we change query params, Ember updates our URL and the property of the controller, but we don't make another call to the server for new data. We can change this behavior by specifying the queryParams which we want to trigger a model refresh.

As we have discussed before, moving forward in Ember, you will also specify queryParam default values in the Route, instead of specifying them on the controller as we have done in the past. But, that behavior is currently behind a feature flag, so we will leave our defaults on the Controller for now.

So, first, let's tell the route that we want to refresh the model when we get new a page param. We don't really need to worry about the size param as that wont be changing. Finally, we will want to change our findAll to a query and pass in our params as JSON API expects them.

app/routes/contacts.js:

import Ember from 'ember';

export default Ember.Route.extend({
  queryParams: {
    page: {
      refreshModel: true
      //defaultValue: 1 <-- what a defaultValue in the Route would look like.
    }
  },
  model(params){
    return this.store.query('person', {
      page: {
        number: params.page,
        size: params.size
      }
    });
  }
});

Now, we need to make Ember CLI Mirage pretend to deal with pagination. The first thing we should do is to make Mirage give us more objects. In /mirage/scenarios/default.js change the createList to give us more than 10 objects:

export default function(server) {

  const people = server.createList('person', 40);

  people.forEach((person) => {
    if(person.role === 'teacher'){
      server.createList('experience', 3, { person });
    } else {
      server.createList('course', 5, { person });
    }
  });
}

Now, let's open up our mirage config in /mirage/config.js and make it respond to our pagination queryParams.

First, let's create some variables for our data, we want one for our page number, one for size, and one for our models, and finally one where we will get our total number of pages.

Now, let's return the paginated data.

this.get('/people', function(schema, request){
  const pageNumber = Number(request.queryParams["page[number]"]);
  const pageSize   = Number(request.queryParams["page[size]"]);
  const people     = schema.people.all();
  const totalPages = Math.ceil(people.models.length/pageSize);

  return setPagination(people, pageNumber, pageSize);
});

We'll create a function for returning a subset of our models. Let's give our function some fancy default values for the pagination variables. Now, we can get the index of the first and last models that we are going to return and slice the model properly and return our new value.

function setPagination(people, pageNumber = 1, pageSize = 10){
  let newReturn = people;
  const lastPersonIndex  = (pageNumber * pageSize);
  const firstPersonIndex = lastPerson - pageSize;
  newReturn["models"] = people.models.slice(firstPerson, lastPerson);
  return newReturn;
}

The last thing we need to do here in mirage is add some meta data for our pagination. Technically, according to the JSON API spec we should have links, but as we aren't going to use them anyway right now, so we'll skip it.

this.get('/people', function(schema, request){
  const pageNumber    = Number(request.queryParams["page[number]"]);
  const pageSize      = Number(request.queryParams["page[size]"]);
  const people        = schema.people.all();
  const totalPages    = Math.ceil(people.models.length/pageSize);
  let paginatedPeople = setPagination(people, pageNumber, pageSize);
  let json            = this.serialize(paginatedPeople);
  json.meta           = { page: pageNumber, total: totalPages };

  return json;
});

Now Mirage should respond to our pagination queryParams, let's add pagination links. We can implement these in a really simple way at first. In out controller we can create a couple of computed properties and an action to update the page property:

app/controllers/contacts.js:

import Ember from 'ember';

export default Ember.Controller.extend({
  queryParams: ['page', 'size'],
  page: 1,
  size: 10,
  previousPage: Ember.computed('page', function(){
    return (this.get('page') > 1) ? this.get('page') - 1 : 1;
  }),
  nextPage: Ember.computed('page', 'model', function(){
    return (this.get('page') < this.get('model.meta.total')) ? this.get('page') + 1 : this.get('model.meta.total');
  }),
  actions: {
    paginate(page){
      this.set('page', page);
    }
  }
});

Now, let's modify our template and use this action and these new computed properties.

app/templates/contacts.hbs

<div class='col-md-3'>
  <div class="list-group">
    {{#each model as |contact|}}
      {{#link-to 'contacts.contact' contact class="list-group-item"}}{{contact.fullName}}{{/link-to}}
    {{/each}}
  </div>
  <div class='pagination-links'>
    <div class="btn-group">
      <button class='btn btn-primary' {{action 'paginate' 1}}>&lt;&lt;</button>
      <button class='btn btn-primary' {{action 'paginate' previousPage}}>&lt;</button>
    </div>
    {{page}} of {{model.meta.total}}
    <div class="btn-group">
      <button class='btn btn-primary' {{action 'paginate' nextPage}}>&gt;</button>
      <button class='btn btn-primary' {{action 'paginate' model.meta.total}}>&gt;&gt;</button>
    </div>
  </div>
</div>
</div>
<div class='col-md-9'>
  {{outlet}}
</div>

We could certainly improve on this, but now we have working pagination. That's it for today, see you tomorrow.

Links