Sync table state with query parameters

This tutorial fits ember-models-table version 2.1.0 or higher. We’ll show only models-table-server-paginated.

Let’s define the task. We have to get initial state of table from the query parameters for page with the table rendered. Additionally, this query parameters should be updated after user interacts with table. What data can we get from the URL? There are page size, current page number, filters, sorting and hidden columns.

Here we wil again use GitHub API. However for this tutorial our requests will address (api/search-repo).

Let’s define Models repo and user for the repository and its owner.

1
2
3
4
5
6
7
8
9
10
11
12
13
// app/models/user.js
import Model from "ember-data/model";
import attr from "ember-data/attr";
import { belongsTo, hasMany } from "ember-data/relationships";
 
export default Model.extend({
  login: attr('string'),
  avatar_url: attr('string'),
  url: attr('string'),
  html_url: attr('string'),
  type: attr('string'),
  site_admin: attr('boolean')
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app/models/repo.js
import Model from "ember-data/model";
import attr from "ember-data/attr";
import { belongsTo, hasMany } from "ember-data/relationships";
 
export default Model.extend({
  name: attr('string'),
  full_name: attr('string'),
  description: attr('string'),
  fork: attr('boolean'),
  owner: belongsTo('user'),
  stargazers_count: attr('number'),
  forks_count: attr('number'),
  open_issues_count: attr('number')
});

Query parameters have to be defined in the Controller and Route (optional). We are going to use both of them in our case.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// app/controllers/table.js
import Ember from 'ember';
 
export default Ember.Controller.extend({
  queryParams: [
    'page',
    'per_page',
    'search',
    'hidden',
    'sort',
    'order',
    'sorted',
    'filters'
  ]
});
1
2
3
4
5
6
7
8
9
10
11
12
13
// app/routes/table.js
import Ember from 'ember';
 
export default Ember.Route.extend({
  queryParams: {
    per_page: {refreshModel: true},
    page: {refreshModel: true},
    search: {refreshModel: true},
    sort: {refreshModel: true},
    order: {refreshModel: true},
    filters: {refreshModel: true}
  }
});

Briefly about each of them:

  • per_page – records count for one page
  • page – current page number
  • search – global filter
  • sort – field name for sorting
  • order – sort order
  • sorted – index of sorted column
  • hidden – list of hidden columns
  • filters – column filters

Flag refreshModel is set for almost all query parameters. It means any change will cause recalling of Route’s hooks model and setupController. Default values for query parameters are defined in the page-controller:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// app/controllers/table.js
import Ember from 'ember';
 
export default Ember.Controller.extend({
  queryParams[/*...*/],
  page: 1,
  per_page: 10,
  search: '',
  hidden: '',
  sort: '',
  order: 'desc',
  sorted: '',
  filters: ''
});

Ember determines type of query-param based on its default value. So, all of them are numbers or strings. Parameters filters and hidden can be defined as arrays (but it doesn’t matter). We can define hidden as comma-separated list of the hidden column numbers. In other words, hidden is a list of column indexes from columns. filters has a more complex structure. Not only column indexes are needed for it. We also need a filter value and a filtered property name. All of them will be combined in the strings like index1:prop1:val1,index2:prop2:val2. Index with filter value will be used on column init and pair “propertyName-filterValue” will be used for request to the server.

model-hook does data loading. It uses method query from the Store (actually, models-table-server-paginated works only with query). All query parameters are available in the first argument for model.

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
38
39
40
41
42
43
44
45
// app/routes/table.js
import Ember from 'ember';
export default Ember.Route.extend({
  hidden: '',
  order: '',
  sorted: '',
  filters: '',
  model(qp) {
    const {page, per_page, search, sort, order, sorted, filters} = qp;
    const query = {page, per_page, search};
    filters.split(',').map(f => {
      const [id, prop, val] = f.split(':');
      query[prop] = val;
    });
    if (sort && sorted) {
      query.sort = sort;
      query.order = order;
      this.setProperties({sorted, order});
    }
    this.setProperties({
      hidden: qp.hidden,
      filters
    });
    return this.get('store').query('repo', query);
  },
  setupController(controller) {
    this._super(...arguments);
    const columns = controller.get('columns');
    this.get('hidden').split(',').map(index => columns[index].isHidden = true);
    const sorted = this.get('sorted');
    const order = this.get('order');
    if (sorted && order) {
      columns[sorted].sortDirection = order;
      columns[sorted].sortPrecedence = 1;
    }
    const filters = this.get('filters');
    if (filters) {
      filters.split(',').forEach(f => {
        const [index, prop, val] = f.split(':');
        columns[index].filterString = val;
      });
    }
    controller.set('columns', columns);
  }
});

query method does data loading. However, table itself doesn’t know its own initial state. This means that we have to set that some column is sorted, filtered or hidden. There is no sense to use propertyName, filteredBy or sortBy.

Even now we don’t know how our columns are defined in the Controller. However we already use them. Ok, there are only five columns:

1
2
3
4
5
6
7
8
9
[
  {propertyName: 'name', disableSorting: true, disableFiltering: true},
  {propertyName: 'owner.login', title: 'Owner', disableSorting: true, filteredBy: 'user'},
  {propertyName: 'stargazers_count', title: 'Stars', sortedBy: 'stars', filteredBy: 'stars',
    filterWithSelect: true, predefinedFilterOptions: ['>0', '0..10', '11..50', '51..100', '100..500', '>500']},
  {propertyName: 'forks_count', title: 'Forks', filteredBy: 'forks',
    filterWithSelect: true, predefinedFilterOptions: ['>0', '0..10', '11..50', '51..100', '100..500', '>500']},
  {propertyName: 'open_issues_count', title: 'Open Issues', disableSorting: true}
]

As we mentioned before, six query parameters have refreshModel set to true and hooks model and setupController will be called after this parameters become changed. It’s expected behavior for model but useless for setupController. Why? It’s useless because changes for controller.columns after models-table-server-paginated init won’t do anything. Table component has its own columns set based on initial columns value and table component works with it. Only first setupController execution is important. This method is light and does not perform a large number of calculations, so we can leave it as is.

We have to define Adapter and Serializer for GitHub API as we did in the previous examples.

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
// app/adapters/repo.js
import Ember from 'ember';
import DS from 'ember-data';
 
export default DS.RESTAdapter.extend({
  host: 'https://api.github.com',
  namespace: 'search/repositories',
  query(store, b, query) {
    const url = `${this.get('host')}/${this.get('namespace')}`;
    let q = 'language:javascript';
    const _query = {...query};
    if (_query.search) {
      q = `${q} ${_query.search}`;
      delete _query.search;
    }
    if (_query.order) {
      _query.order = _query.order.toLowerCase();
    }
    ['stars', 'forks', 'user'].forEach(propertyName => {
      if (_query[propertyName]) {
        q = `${q} ${propertyName}:${_query[propertyName]}`;
        delete _query[propertyName];
      }
    });
    _query.q = q;
    store.serializerFor('repo').set('pageSize', query.per_page);
    return this.ajax(url, 'GET', {data: _query});
  }
});

Only single method query is defined here. It does simple API-request (nothing special).

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
38
39
40
41
42
// app/serializers/repo.js
import DS from 'ember-data';
 
export default DS.JSONAPISerializer.extend({
  pageSize: 10,
  _mapIncluded(data, type) {
    const clb = item => ({
      id: item.id,
      type,
      attributes: item
    });
    return Ember.isArray(data) ? data.map(clb) : clb(data);
  },
 
  normalizeQueryResponse(a, b, payload) {
    const included = [];
    const newPayload = {
      data: payload.items.map(item => {
        const itemData = {
          id: item.id,
          type: 'repo',
          attributes: item,
          relationships: {
            owner: {
              data: { id: item.owner.id, type: 'user' }
            }
          }
        };
        included.pushObject(this._mapIncluded(item.owner, 'user'));
        return itemData;
      })
    };
    newPayload.included = included;
    // github api won't allow to search more 1000 items
    const itemsCount = payload.total_count > 1000 ? 1000 : payload.total_count;
    newPayload.meta = {
      itemsCount,
      pagesCount: Math.ceil(itemsCount / this.get('pageSize'))
    };
    return newPayload;
  }
});

Method normalizeQueryResponse will be called to proceed server response for query-request. That is why it’s defined in the Serializer.

For current moment our code can set initial state for table. Now we must define a way to update query parameters on user interaction.

Components models-table and models-table-server-paginated can create two events. First one appears when hidden-state is toggled for some column or columns set. Second one appears after filtering, sorting or table navigation. We can define handlers for this events in the parent scope (page-controller).

Event displayDataChanged (or any other defined in the displayDataChangedAction) sends a little bit more data. Filters and sorting states where available in the fields sort and columnsFilters and now you can get them from field columns too. This field is an array with data for all columns. Its each element is an object with five properties:

  • filterString
  • filterField
  • sortField
  • sorting
  • propertyName

These are all properties of the same name from ModelsTableColumn (check docs).

Event handlers:

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
38
39
40
41
42
43
44
// app/controllers/table.js
import Ember from 'ember';
export default Ember.Controller.extend({
  actions: {
    displayAction(d) {
      const sorted = d.columns.find(_ => _.sorting !== 'none');
      if (sorted) {
        this.setProperties({
          sort: sorted.sortField,
          order: sorted.sorting,
          sorted: d.columns.indexOf(sorted)
        });
      }
      else {
        this.setProperties({
          sorted: '',
          order: 'desc',
          sort: ''
        });
      }
      const filters = [];
      d.columns.forEach((c, i) => {
        if (c.filterString) {
          filters.pushObject(`${i}:${c.filterField}:${c.filterString}`);
        }
      });
      this.setProperties({
      	page: get(d, 'currentPageNumber'),
        per_page: get(d, 'pageSize'),
        search: get(d, 'filterString'),
        filters: filters.join(',')
      });
    },
    visibilityAction(d) {
      const hidden = [];
      d.forEach((c, i) => {
        if (c.isHidden) {
          hidden.pushObject(i);
        }
      });
      this.set('hidden', hidden);
    }
  }
});

Here we set new values for the Controller properties corresponding to the query parameters. New request will be send on displayAction. Now request will be send on visibilityAction, because hidden doesn’t have a refreshModel in the Route.

Table renderind is pretty simple:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{{! app/templates/table.hbs }}
{{models-table-server-paginated
  data=model
  columns=columns
  themeInstance=themeInstance
  filterQueryParameters=filterQueryParameters
  currentPageNumber=page
  pageSize=per_page
  filterString=search
  displayDataChangedAction="displayAction"
  columnsVisibilityChangedAction="visibilityAction"
  sendColumnsVisibilityChangedAction=true
  sendDisplayDataChangedAction=true
}}

Demo is available on the ember-twiddle – Table settings from query params. You will be redirected to the table with a predefined query parameters.

1
filters=2:stars:>0&hidden=4&page=2&per_page=25&search=ember&sort=stars&sorted=2

As you can see, table is paginated to the second page with 25 elements. Also table is sorted and filtered by third column and its fifth column is hidden.

, , ,

Add comment

Top ↑ | Main page | Back