Синхронизация состояния таблицы с параметрами запроса страницы

Данный материал расчитан на ember-models-table версии 2.1.0 и выше. Мы рассмотрим только таблицу models-table-server-paginated.

Сформулируем задачу. Нам надо дать возможность получить исходное состояние таблицы из параметров запроса страницы, где она отрисована. Так же необходимо обновлять значения параметров запроса, когда пользователь взаимодействует с таблицей. Какие данные мы можем взять из URL? Это размер страницы, номер текущей страницы, состояние сортировки и фильтров, а так же скрытые колонки.

Как и в других примерах мы воспользуемся GitHub API, но на этот раз возьмем поиск по репозиториям (api/search-repo).

Опишем Модели repo и user, соответствующие репозиторию и его владельцу.

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

Чтоб использовать параметры запроса для страницы, их надо для начала описать в соответствующем Контроллере и (при необходимости) в Роуте. В нашем случае, нам понадобятся оба — и Роут, и Контроллер.

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

Кратко о каждом из них:

  • per_page — кол-во элементов на одной странице таблицы
  • page — номер текущей страницы таблицы
  • search — «общий» фильтр
  • sort — поле, по которому выполнена сортировка
  • order — направление сортировки
  • sorted — колонка, по которой выполнена сортировка
  • hidden — скрытые колонки
  • filters — фильтры по колонкам

Мы указали флаг refreshModel почти для всех параметров запроса. Это значит, что их изменение приведет к повторному вызову model и setupController Роута. Значения по умолчанию для параметров запроса выглядят так:

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

Эмбер определяет тип параметра запроса исходя из его начального значения. В нашем случае получается, что все они либо числа, либо строки. Параметры filters и hidden можно было бы сделать и массивами, но это не принципиально. В hidden мы можем через запятую перечислить номера скрытых в данный момент колонок. Другими словами, hidden — это индексы колонок из columns. У filters будет более сложная структура. Помимо индексов нам нужно еще значение фильтра и поле, к которому он применяется. Мы это все сводим к строке вида index1:prop1:val1,index2:prop2:val2. Индекс и значение фильтра мы используем при инициализации колонки, а пару «свойство-значение» — для составления запроса на сервер.

В хуке model Роута выполняется загрузка данных. Для этого используется метод query Хранилища (а с любым другим models-table-server-paginated работать не будет). Все параметры запроса доступны через первый аргумент 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 Хранилища корректно выполнит загрузку данных. Но, сама по себе таблица не знает, в каком она состоянии на момент инициализации. То есть, нам надо явно указать, что для каких-то колонок выполнены фильтрация и сортировка, а какие-то колонки скрыты. Для этого используется setupController. Все три случая обработаны с использованием индексов колонок. Нет смысла привязываться к propertyName, filteredBy или sortedBy.

Даже в данный момент мы еще не знаем, а как, собственно, объявлены колонки в Контроллере, но уже оперируем ими. Что ж, колонок будет всего пять:

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

Так как для шести параметров запроса указан флаг refreshModel, то хуки model и setupController будут вызываться при любом их (параметров) изменении. Это требуемое поведение для model, но бесполезное для setupController. Почему? Потому что изменение controller.columns после инициализации models-table-server-paginated уже ни к чему не приведет. Внутри самого Компонента таблицы будет высчитан свой массив с колонками и таблица будет работать с ним. Срабатывание setupController важно только первый раз. Так как этот метод достаточно легкий и не совершает большого количества вычислений, то его можно оставить как есть.

Как и предыдущих примерах с GitHub API, нам надо описать еще Адаптер и Сериализатор для Модели repo:

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

Переопределяем единственный метод 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
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;
  }
});

Метод normalizeQueryResponse будет вызван для обработки ответа на «query-запрос», потому мы его переопределяем под наши нужды.

В данный момент наш код уже умеет задать начальное состояние таблицы. Теперь надо сделать так, чтоб мы могли обновлять параметры запроса, когда пользователь как-либо взаимодействует с таблицей.

Компоненты models-table и models-table-server-paginated могут создавать два события. Первое возникает, если какая-то колонка или группа колонок скрываются или же становятся видимыми, а второе можно получить при фильтрации, навигации или сортировке таблицы. В Контроллере страницы с таблицой надо перехватывать оба этих события.

В версии 2.1.0 событие displayDataChanged (или другое, указанное в displayDataChangedAction) отправляет чуть больше данных. Раньше фильтры и сортировки были доступны только через поля sort и columnsFilters, а сейчас их можно получить и через поле columns. Это поле является массивом с данными по всем колонкам таблицы. Каждый элемент — это объект с пятью полями:

  • filterString
  • filterField
  • sortField
  • sorting
  • propertyName

Это все одноименные свойства из ModelsTableColumn (см. доки).

Код перехватчиков этих событий:

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

Здесь мы задаем новые значения для свойств Контроллера, соответствующих параметрам запроса страницы. При срабатывании displayAction, будет выполнен запрос на GitHub API за новыми данными. При срабатывании visibilityAction такого запроса не будет, так как hidden не объявлено в Роуте с флагом refreshModel.

Отрисовка таблицы на странице как обычно происходит без какой-либо «магии»:

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

Как обычно демо доступно на ember-twiddle Table settings from query params. По загрузке сразу происходит перенаправление на Роут /table с предопределенными параметрами запроса:

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

Как видно, таблица сразу выводит вторую страницу размером 25 элементов. Она отсортирована и отфильтрована по третьей колонке, а ее пятая колонна скрыта.

, , ,

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

Top ↑ | Main page | Back