Данный материал расчитан на 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 элементов. Она отсортирована и отфильтрована по третьей колонке, а ее пятая колонна скрыта.
addon, ember, ember-models-table, table
Советы и приемы по работе с ember-models-table
- Переход с первой версии ember-models-table на вторую
- Темизация в ember-models-table 2.x
- Использование вложенных таблиц ember-models-table
- Дополнительные запросы при загрузке данных для models-table-server-paginated
- Синхронизация состояния таблицы с параметрами запроса страницы
- Группировка строк в таблицах
- Редактирование данных в таблице и немного агрегативки
- Навигация с использованием клавиатуры в ember-models-table
- Асинхронная загрузка опций для фильтров в ember-models-table
- Контекстное меню для ember-models-table
Оставить комментарий