Additional requests in the models-table-server-paginated for data loading

Component’s models-table-server-paginated work for data loading is based on method query in the Store. More precisely, Component expects that server returns not only data for current page to show and meta-info with overall records count too. This values is used in the summary and to calculate last page number for table.

What have we do if query-response doesn’t contain this meta-info, however another API end-point has it? We have only one way – do additional request before any data loading.

Method doQuery was added to the models-table-server-paginated in the final 2.0.0 release:

1
2
3
4
5
6
7
8
9
10
/**
 * @param {DS.Store} store
 * @param {string} modelName
 * @param {object} query
 * @returns {Promise}
 */
doQuery(store, modelName, query) {
  return store.query(modelName, query)
    .then(newData => set(this, 'filteredContent', newData));
},

Method loads data from server and save it to the filteredContent-property. We have to override this method to add another request here. This means that we have to create our own Component that extends models-table-server-paginated. Since we are working through the Store, then we definitely have an Adapter and a Serializer for the Model, the Records of which are shown in the table. So, extra-request may be done in the Adapter and received data should be set directly to the Serializer.

Let’s check a real example with a real API. However, for educational purposes, it is necessary to go for some restrictions. We’ll work with a GitHub API for repository issues. There is a simple GET-request for all open issues for selected repo – https://api.github.com/repos/{owner}/{repo}/issues (api/issues). It returns 30 items or less by default. And that’s it. There are no any meta-info. All open issues count exists in the response for GET-request to https://api.github.com/repos/{owner}/{repo} (api/repos). Field is called open_issues_count. Well, we decided on the requests. It’s time to define Model, Adapter and Serializer.

Let’s start with Model. It’s called issue (we won’t be particularly original):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app/models/issue.js
import Model from "ember-data/model";
import attr from "ember-data/attr";
 
export default Model.extend({
  number: attr('number'),
  title: attr('string'),
  state: attr('string'),
  comments: attr('number'),
  user: attr('string'),
  type: attr('string'),
  labels: attr('string'),
  state: attr('string')
});

It’s a typical Model with several attributes and without any relations.

We have to define a query-method and one extra-method called loadRepoInfo in the our Adapter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// app/adapters/issue.js
import DS from 'ember-data';
 
export default DS.RESTAdapter.extend({
  host: 'https://api.github.com',
  namespace: 'repos/emberjs/ember.js',
  query(store, modelName, query) {
    const url = `${this.get('host')}/${this.get('namespace')}/issues`;
    store.serializerFor('issue').set('pageSize', query.per_page);
    return this.ajax(url, 'GET', {data: query});
  },
  loadRepoInfo(store) {
    return this.ajax(`${this.get('host')}/${this.get('namespace')}`, 'GET')
      .then(d => store.serializerFor('issue').set('issuesCount', d.open_issues_count));
  }
});

We’ll work with data about ember.js repo.

query-method works as expected – it does a GET-request to the provided URL with the given parameters. loadRepoInfo also does a GET-request and put received open_issues_count to the Serializer’s issuesCount-property. Method Store.serializerFor is used for this.

Serializer issue has only one method called normalizeQueryResponse (because we do a query-request):

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
// app/serializers/issue.js
import DS from 'ember-data';
 
export default DS.JSONAPISerializer.extend({
  issuesCount: 10,
  pageSize: 10,
  normalizeQueryResponse(store, modelClass, payload) {
    const issuesCount = this.get('issuesCount');
    const newPayload = {
      data: payload.map(item => {
        const itemData = {
          id: item.id,
          type: 'issue',
          attributes: item
        };
        itemData.attributes.type = item.hasOwnProperty('pull_request') ? 'Pull Request' : 'Issue';
        itemData.attributes.user = item.user.login;
        itemData.attributes.labels = item.labels.mapBy('name').join(', ');
        return itemData;
      });
    };
    newPayload.meta = {
    	itemsCount: issuesCount,
      pagesCount: Math.round(issuesCount / this.get('pageSize'))
    };
    return newPayload;
  }
});

As usual, the Serializer methods format response into the JSON-API format. We use issuesCount value to create a meta-info. Initially, issuesCount is set to 10, but it will be update by Adapter even before first call of normalizeQueryResponse.

First time value for issuesCount should be loaded before transition to page with issues table is complete:

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
// app/routes/index.js
import Ember from 'ember';
 
export default Ember.Route.extend({
  beforeModel() {
    const store = this.get('store');
    return store.adapterFor('issue').loadRepoInfo(store);
  },
  model() {
    return this.get('store').query('issue', {per_page: 10});
  },
  setupController(controller) {
    this._super(...arguments);
    controller.set('columns', [
      {propertyName: 'number', title: 'ID', useSorting: false},
      {propertyName: 'type', useSorting: false},
      {propertyName: 'user', useSorting: false},
      {propertyName: 'title', useSorting: false},
      {propertyName: 'labels', useSorting: false},
      {propertyName: 'comments', useSorting: false}
    ]);
    controller.set('filterQueryParameters', {
      pageSize: 'per_page',
      page: 'page'
    });
  }
});

Route’s hooks are executed one after another in a strict order and only after the completion of the previous ones (Promises are handled too). That’s why we can use beforeModel to execute loadRepoInfo.

Keep in mind that Adapter is available via the Store.adapterFor.

Property filterQueryParameters will be passed to the out table-component and contains names for fields used for server pagination. In this case we set only current page number and its size (api/pagination).

Last but not least is our Component called my-table. It’ll extend original models-table-server-paginated:

1
2
3
4
5
6
7
8
9
10
11
// app/components/my-table.js
import Ember from 'ember';
import ModelsTableServerPaginated from './models-table-server-paginated';
 
export default ModelsTableServerPaginated.extend({
  doQuery(store, modelName, query) {
    const sup = this._super;
    return store.adapterFor('issue').loadRepoInfo(store)
    	.then(d => sup.call(this, store, modelName, query));
  }
});

Check how we call sup. We store a reference to it and call it in an explicitly defined context after method loadRepoInfo is executed. More info here – ember.js/issues#13280.

Now we can render our table:

1
2
3
4
5
6
7
8
9
{{! app/templates/index.hbs}}
{{my-table
  data=model
  columns=columns
  filterQueryParameters=filterQueryParameters
  useFilteringByColumns=false
  showGlobalFilter=false
  showColumnsDropdown=false
}}

Finally we get a table that shows open issues for ember.js-repo.

Demo is available on ember-twiddle – Server paginated table (BS4). Demo contains a small bonus – table automatically scrolls to top after page is changed.

, , ,

Tips and tricks for working with ember-models-table 

Add comment

Top ↑ | Main page | Back