Давайте, добавим на страницу нашего дополнения объект типа GridPanel (xtype: grid).
У таблицы обязательными параметрами являются columns — список колонок и store — собственно список данных, которые нужно отобразить в таблице.
ExtJS Store
Параметр store есть у многих объектов в ExtJS. Обычно, если нужно отобразить какой-то список, используется параметр store. Это может быть как таблица, так и выпадающий список элементов (как в админке MODX выпадает список шаблонов при редактировании ресурса). Ещё есть, например ListView (xtype: listview), который просто выводит список элементов без возможности редактировать.
Из-за того, что таких типов несколько (а мы можем ещё и свои собственные типы создавать), store — это не просто массив, а тоже объект со своими свойствами. В ExtJS для store есть несколько типов объектов:

И не забывайте, что вы можете создавать свои типы объектов на основе существующих или вообще с нуля. Но для простой таблички нам хватит того, что уже есть. Возьмём, к примеру, ArrayStore (xtype: arraystore). В нём нужно в первую очередь перечислить список полей, которые содержатся в массиве, и, собственно, массив данных:
// ...
}, {
xtype: 'grid',
columns: [ // Перечисляем список столбцов
{dataIndex: 'id'},
{dataIndex: 'name'}
],
store: new Ext.data.ArrayStore({ // Объект ArrayStore
fields: ['id','name'], // Поля, доступные в массиве данных
data: [ // Собственно, массив данных ([id, name])
[1, 'Pencil'],
[2, 'Umbrella'],
[3, 'Ball'],
]
})
}
// ...Получившийся табличкой не очень удобно пользоваться =))
Это потому что мы не указали таблице высоту. Можно воспользоваться параметром height и указать высоту таблицы в пикселях, но лучше давайте скажем ExtJS, чтобы он подстроил высоту таблички под имеющиеся в ней данные:
autoHeight: true,
Уже лучше, но теперь хочется растянуть табличку ещё и по ширине. Для управлением внешним видом таблички есть параметр viewConfig
viewConfig: {
forceFit: true
}Данные в таблицах бывают разные и ширина столбцов не должна быть одинаковой — ведь в каком-то столбце могут быть только числа, а в каком-то может быть длинный текст. У колонок в ExtJS можно задать ширину в пикселях. А если указан параметр forceFit, то ширина колонок будет пропорциональна числу, указанному в параметре width.
Поэтому, возьмём общую условную ширину таблицы за 1000 пикселей и укажем ширину столбцов, соответствующую такой таблице. А уже ExtJS растянет её на всю ширину экрана самостоятельно. Я сделаю пропорцию столбцов 33% — 67%. А ещё, давайте укажем названия столбцов:
// ...
}, {
xtype: 'grid',
columns: [ // Добавляем ширину и заголовок столбца
{dataIndex: 'id', width: 330, header: 'ID'},
{dataIndex: 'name', width: 670, header: 'Name'}
],
autoHeight: true, // Высота таблицы вычисляется автоматически
viewConfig: {
forceFit: true, // Растягиваем таблицу на всю ширину
scrollOffset: 0 // Убираем вертикальный скролл (у нас же автовысота)
},
store: new Ext.data.ArrayStore({
fields: ['id','name'],
data: [
[1, 'Pencil'],
[2, 'Umbrella'],
[3, 'Ball'],
]
})
}
// ...У MODX есть свой объект таблицы с прописанными свойствами, методами и оформлением — MODx.grid.Grid попробуем создать свою табличку на основе этой заготовки.
Сначала опишем свойства нашего объекта в конце файла (там, где заканчивается Things.panel.Home). В объекте Things у нас уже есть вложенный объект gird, вот внутри него и создадим нашу табличку:
// ...
}, { // Убираем все свойства в описание объекта
xtype: 'things-grid-names' // Оставляем только xtype, заменив его на наш собственный
}
// ...
// Внутри Things есть grid. Вот внутри него и создаём свой объект
Things.grid.Names = function (config) { // Придумываем название, например, «Names»
config = config || {};
Ext.apply(config, {
// Сюда перемещаем все свойства нашей таблички
columns: [
{dataIndex: 'id', width: 330, header: 'ID'},
{dataIndex: 'name', width: 670, header: 'Name'}
],
autoHeight: true,
viewConfig: {
forceFit: true,
scrollOffset: 0
},
store: new Ext.data.ArrayStore({
fields: ['id','name'],
data: [
[1, 'Pencil'],
[2, 'Umbrella'],
[3, 'Ball'],
]
})
});
Things.grid.Names.superclass.constructor.call(this, config); // Магия
}
Ext.extend(Things.grid.Names, Ext.grid.GridPanel); // Наша табличка расширяет GridPanel
Ext.reg('things-grid-names', Things.grid.Names); // Регистрируем новый xtypeМы можем зарегистрировать любой xtype, например, 'my-super-puper-table'. Но чтобы потом не запутаться в своих xtype, лучше в их названиях отображать структуру объектов.
Теперь попробуем изменить объект, который будет расширять наша табличка:
// ... Ext.extend(Things.grid.Names, MODx.grid.Grid); // ...
Объект MODx.grid.Grid подразумевает, что store не прописывается в файле ввиде массива, а запрашивает данные у сервера. Поэтому нужно указать адрес нашего коннектора и action, который будет запрашиваться, а также сюда перенесём список полей:
// ... url: Things.config.connector_url, action: 'mgr/thing/getlist', fields: ['id','name'] // ...
Свойство store удаляем — оно нам больше не нужно.
Чтобы вместо надписи Request completed появился список записей, меняем код процессора:
<?php
class ThingsGetListProcessor extends modProcessor {
public function process() {
return json_encode(array(
'success' => true,
'total' => 3,
'results' => array(
array('id' => 1, 'name' => 'Pencil'),
array('id' => 2, 'name' => 'Umbrella'),
array('id' => 3, 'name' => 'Ball')
)
));
}
}
return "ThingsGetListProcessor";Если в таблице предполагается большое количество записей, принято не выводить их все, а реализовать пагинацию. Нам для этого делать ничего не надо — пагинацией пусть занимается ExtJS. Просто добавим параметр paging
// ... paging: true, pageSize: 1, // количество записей на странице // ...
Осталось научить процессор работать с параметрами start и limit:
<?php
class ThingsGetListProcessor extends modProcessor {
public function process() {
$array = array();
switch ($this->getProperty('start')) {
case 0:
$array[] = array('id' => 1, 'name' => 'Pencil');
break;
case 1:
$array[] = array('id' => 2, 'name' => 'Umbrella');
break;
case 2:
$array[] = array('id' => 3, 'name' => 'Ball');
break;
default:
break;
}
return json_encode(array(
'success' => true,
'total' => 3,
'results' => $array
));
}
}
return "ThingsGetListProcessor";Конечно, данные в процессоре нужно получать из базы данных, а параметры start и limit передавать в запрос. Для этого в MODX тоже есть свои заготовки. Мы рассмотрим их в следующей части.
Объектная
// Ext.onReady(function() { // Когда страница загрузилась var Things = function (config) { config = config || {}; Things.superclass.constructor.call(this, config); }; Ext.extend(Things, MODx.Component, { // Перечисляем группы, внутрь которых будем "складывать" объекты panel: {}, page: {}, window: {}, grid: {}, tree: {}, combo: {}, config: {}, view: {}, utils: {} }); Ext.reg('things', Things); // Мы не будем вставлять компонент на страницу с помощью MODx.load, // поэтому нужно создать экземпляр нашего класса // чтобы можно было обращатсья к его свойствам Things = new Things(); // Создаём внутри компонента главную панель // (через точку в JS обозначется вложенность массива, то есть мы создаём // объект Home внутри panel, который, в свою очередь, находится в Things) Things.panel.Home = function (config) { config = config || {}; Ext.apply(config, { // xtype можно не указывать, так как внутри MODx.Panel xtype уже указан // А вот если вы захотите xtype изменить, его можно указать здесь renderTo: 'modx-panel-holder', cls: 'container', // Добавляем отступы items: [{ html: '<h2>Things</h2>' },{ xtype: 'modx-tabs', // Добавляем объект табов в нашу панель paging: true, pageSize: 1, // количество записей на странице items: [{ // Перечисляем, какие именно табы нам нужны title: 'Things 1', // Заголовок первого таба items: [{ // А внутри таба HTML-блок с классом panel-desc html: 'Things 1 description', cls: 'panel-desc', },{ xtype: 'panel', cls: 'container', items: [{ xtype: 'button', text: 'Load', cls: 'primary-button', handler: function() { MODx.Ajax.request({ url: Things.config.connector_url, // запрос пойдёт на адрес /connectors/index.php params: { // указываем параметры запроса action: 'mgr/thing/getlist', // здесь путь к процессору // можно указать и дополнительные параметры запроса parent: 0, fields: ['id','name'] }, listeners: { success: { // при успешном запросе fn: function ( r ) { console.log( r ); // выведем ответ в консоль и покажем окошко Ext.MessageBox.alert('Load', JSON.stringify(r.results)); }, scope: this } } }); } }, { xtype: 'things-grid-names' } ] } ] }, { title: 'Things 2', // Заголовок второго таба items: [{ // Внутри таба ещё один HTML-блок с классом panel-desc html: 'Things 2 description', cls: 'panel-desc', } ] } ] } ] }); Things.panel.Home.superclass.constructor.call(this, config); // Опять магия }; Things.grid.Names = function (config) { // Придумываем название, например, «Names» config = config || {}; Ext.apply(config, { // Сюда перемещаем все свойства нашей таблички columns: [ {dataIndex: 'id', width: 330, header: 'ID'}, {dataIndex: 'name', width: 670, header: 'Name'} ], autoHeight: true, viewConfig: { forceFit: true, scrollOffset: 0 } }); Things.grid.Names.superclass.constructor.call(this, config); // Магия } Ext.extend(Things.grid.Names, MODx.grid.Grid); // Наша табличка расширяет GridPanel Ext.reg('things-grid-names', Things.grid.Names); // Регистрируем новый xtype Ext.extend(Things.panel.Home, MODx.Panel); // Наша панель расширяет объект MODX.Panel Ext.reg('things-panel-home', Things.panel.Home); // Регистрируем новый xtype для панели Ext.onReady(function() { MODx.load({ // На странице выводим сразу нашу панель xtype: 'things-panel-home' }); });Не могли бы вы в конце писать итоговое, что получилось.
А то всё было понятно, и даже очень(в разы лучше, чем у Безумкина), но вот после этих сокращений…
понять что и где происходит дальше становится тяжко(мне, как новичку).
Благодарю.
Илья, не подскажешь как можно менять заголовки у таблиц в ExtJS? Т.е. мне нужно, чтобы columns и fields менялись при пагинации, например! Ну скажем по принципу календаря, я хочу выводить даты в виде заголовков. Ну или например, если я из процессора помимо results передам еще и headers, как их можно вытащить из интерфейса ExtJS и подставить в columns и fields?
Например, отсюда:
{"success":true,"total":"74","headers":[{"header":"2020-01-07","dataIndex":"2020-01-07"},{"header":"2020-01-08","dataIndex":"2020-01-08"]},"results":[{ ... ]};Написал пример на codepen. Там закомментированный код именно это и делает!
miniShop2.grid.Orders = function (config) { config = config || {}; if (!config.id) { config.id = 'minishop2-grid-orders'; } Ext.applyIf(config, { baseParams: { action: 'mgr/orders/getlist', sort: 'id', dir: 'desc', }, multi_select: true, changed: false, stateful: true, stateId: config.id, }); miniShop2.grid.Orders.superclass.constructor.call(this, config); }; Ext.extend(miniShop2.grid.Orders, miniShop2.grid.Default, { getFields: function () { return miniShop2.config['order_grid_fields']; }, getColumns: function () { var all = { id: {width: 35}, customer: {width: 100, renderer: function(val, cell, row) { return miniShop2.utils.userLink(val, row.data['user_id'], true); }}, num: {width: 50}, receiver: {width: 100}, createdon: {width: 75, renderer: miniShop2.utils.formatDate}, updatedon: {width: 75, renderer: miniShop2.utils.formatDate}, cost: {width: 50, renderer: this._renderCost}, cart_cost: {width: 50}, delivery_cost: {width: 75}, weight: {width: 50}, status: {width: 75, renderer: miniShop2.utils.renderBadge}, delivery: {width: 75}, payment: {width: 75}, //address: {width: 50}, context: {width: 50}, actions: {width: 75, id: 'actions', renderer: miniShop2.utils.renderActions, sortable: false}, }; var fields = this.getFields(); var columns = []; for (var i = 0; i < fields.length; i++) { var field = fields[i]; if (all[field]) { Ext.applyIf(all[field], { header: _('ms2_' + field), dataIndex: field, sortable: true, }); columns.push(all[field]); } } return columns; }, getTopBar: function () { return []; }, getListeners: function () { return { rowDblClick: function (grid, rowIndex, e) { var row = grid.store.getAt(rowIndex); this.updateOrder(grid, e, row); }, afterrender: function (grid) { var params = miniShop2.utils.Hash.get(); var order = params['order'] || ''; if (order) { this.updateOrder(grid, Ext.EventObject, {data: {id: order}}); } }, }; }, orderAction: function (method) { var ids = this._getSelectedIds(); if (!ids.length) { return false; } MODx.Ajax.request({ url: this.config.url, params: { action: 'mgr/orders/multiple', method: method, ids: Ext.util.JSON.encode(ids), }, listeners: { success: { fn: function () { //noinspection JSUnresolvedFunction this.refresh(); }, scope: this }, failure: { fn: function (response) { MODx.msg.alert(_('error'), response.message); }, scope: this }, } }) }, updateOrder: function (btn, e, row) { if (typeof(row) != 'undefined') { this.menu.record = row.data; } var id = this.menu.record.id; MODx.Ajax.request({ url: this.config.url, params: { action: 'mgr/orders/get', id: id }, listeners: { success: { fn: function ® { var w = Ext.getCmp('minishop2-window-order-update'); if (w) { w.close(); } w = MODx.load({ xtype: 'minishop2-window-order-update', id: 'minishop2-window-order-update', record: r.object, listeners: { success: { fn: function () { this.refresh(); }, scope: this }, hide: { fn: function () { miniShop2.utils.Hash.remove('order'); if (miniShop2.grid.Orders.changed === true) { Ext.getCmp('minishop2-grid-orders').getStore().reload(); miniShop2.grid.Orders.changed = false; } } }, afterrender: function () { miniShop2.utils.Hash.add('order', r.object['id']); } } }); w.fp.getForm().reset(); w.fp.getForm().setValues(r.object); w.show(e.target); }, scope: this } } }); }, removeOrder: function () { var ids = this._getSelectedIds(); Ext.MessageBox.confirm( _('ms2_menu_remove_title'), ids.length > 1 ? _('ms2_menu_remove_multiple_confirm') : _('ms2_menu_remove_confirm'), function (val) { if (val == 'yes') { this.orderAction('remove'); } }, this ); }, _renderCost: function (val, idx, rec) { return rec.data['type'] != undefined && rec.data['type'] == 1 ? '-' + val : val; }, }); Ext.reg('minishop2-grid-orders', miniShop2.grid.Orders);