Дополнительные запросы при загрузке данных для models-table-server-paginated

Работа Компонента models-table-server-paginated основана на использовании метода query Хранилища для загрузки данных. Точнее, Компонент ожидает, что помимо самих данных возвращается еще и мета-информация об общем количестве записей. Это значение используются в summary, а также для определия номера последней страницы таблицы.

Как быть, если в query-запросе нет этой информации, но она есть в ответе на другой запрос? Ответ один — надо делать этот запрос перед каждой загрузкой данных.

В релиз-версии 2.0.0 у Компонента models-table-server-paginated появился небольшой метод doQuery:

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));
},

Метод загружает данные с сервера и сохраняет их в свойстве filteredContent Компонента. Чтоб вклиниться в этот процесс надо переопределить данный метод (и сделать свой Компонент на основе models-table-server-paginated). Так как мы работаем через Хранилище, то у нас точно есть Адаптер и Сериализатор для Модели, Записи которой выводятся в таблице. «Лишний» запрос лучше делать через Адаптер и полученные данные по количеству записей сразу передавать в Сериализатор.

Рассмотрим реальный пример с реальным API. Правда, в учебных целях, придется пойти на некоторые ограничения. Возьмем запрос из GitHub API, который возвращает список открытых вопросов (issues) для выбранного репозитория. Это обычный GET-запрос на URL https://api.github.com/repos/{owner}/{repo}/issues (см. api/issues). По умолчанию он возвращает обычный массив из 30 или менее элементов. И все. Тут нет никаких данных по общему количеству открытых вопросов. А вот ответ на GET-запрос по ссылке https://api.github.com/repos/{owner}/{repo} содержит поле open_issues_count, в котором и находится количество открытых на данный момент вопросов (см. api/repos). Что ж, с запросами мы определились. Теперь надо написать свои Модель, Адаптер и Сериализатор.

Начнем с Модели. Назовем ее issue (не будем особо оригинальничать):

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')
});

Обычная Модель с несколькими атрибутами и без связей.

В Адаптере нам надо реализовать метод query, а так же еще один «нештатный» метод loadRepoInfo:

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) {
    store.serializerFor('issue').set('pageSize', query.per_page);
    const url = `${this.get('host')}/${this.get('namespace')}/issues`;
    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));
  }
});

Мы будем работать с данными для репозитория ember.js

Метод query работает так, как ему положено по спецификации — делает GET-запрос на указанный URL с заданными параметрами. Метод loadRepoInfo тоже выполняет GET-запрос, но по его завершению, он из полученных данных берет значение open_issues_count и передает его в свойство Сериализатора issuesCount. Для этого используется метод serializerFor Хранилища.

В Сериализаторе issue надо описать метод normalizeQueryResponse (мы же выполняем query-запрос):

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;
  }
});

Как обычно, методы Сериализатора переводят полученный ответ в формат JSON-API. Мы используем значение issuesCount для того, чтоб сформировать правильное поле meta. Изначально issuesCount установлено как 10, но еще до первого вызова normalizeQueryResponse оно должно будет переопределено из Адаптера.

Первый раз надо загрузить issuesCount при переходе на страницу, где выводится таблица с вопросами:

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'
    });
  }
});

Так как хуки Роута выполняются один за другим в строгом порядке и только после завершения предыдущих (с учетом Promise’ов), то мы можем в beforeModel выполнить loadRepoInfo Адаптера.

Обратите внимание на то, что доступ к Адаптеру идет через метод adapterFor Хранилища.

Свойство filterQueryParameters будет передано в наш Компонент-таблицу и содержит в себе имена свойств, используемых для постраничной навигации. В данной ситуации нам надо указывать номер запрашиваемой страницы и ее размер (см. api/pagination).

Осталось описать Компонент my-table, который будет наследником от 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));
  }
});

Обратите внимание на вызов sup. Мы сохраняем ссылку на него и вызываем его в явно заданном контексте уже после выполнения метода loadRepoInfo. Подробнее по ссылке — ember.js/issues#13280

Теперь можно отрисовать нашу таблицу:

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
}}

В результате мы получаем таблицу, которая выводит все открытые вопросы для репозитория ember.js.

Рабочая демка как обычно доступна на ember-twiddle — Server paginated table (BS4). В демке есть небольшой бонус в виде автоскролла таблицы при переходе между страницами.

, , ,

Оставить комментарий

Top ↑ | Main page | Back