Наверх

Как работают дополнения MODX. Часть 4 — ExtJS Grid

Познакомимся с новым типом объектов в ExtJS — таблицей.

Давайте, добавим на страницу нашего дополнения объект типа 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 тоже есть свои заготовки. Мы рассмотрим их в следующей части.


10 комментариев

  1. Саня 28 октября 2017, 19:49 # +1
    Круто! Спасибо, Илья.
    1. Станислав 08 ноября 2017, 13:32 # 0
      Привет, Илья. Посмотри, все ли верно. У меня почему-то не работает. Таблица не выводится, а в консоли ошибка «TypeError: this.proxy is undefined». До добавления объекта MODx.grid.Grid все работает. С ним нет.Спасибо!

      // 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'
          });
      });
      
      
      1. Илья Уткин 08 ноября 2017, 13:41 # +1
        У таблицы MODx.grid.Grid нужно указать url и action
        1. Станислав Однолетко 08 ноября 2017, 13:55 # 0
          Спасибо)
      2. Макс 19 января 2018, 05:09 # 0
        Приветствую.
        Не могли бы вы в конце писать итоговое, что получилось.
        А то всё было понятно, и даже очень(в разы лучше, чем у Безумкина), но вот после этих сокращений…
        // Убираем все свойства в описание объекта
        xtype: 'things-grid-names' // Оставляем только xtype, заменив его на наш собственный
        понять что и где происходит дальше становится тяжко(мне, как новичку).
        Благодарю.
        1. Сергей 17 марта 2020, 13:17(Комментарий был изменён) # 0
          Привет!
          Илья, не подскажешь как можно менять заголовки у таблиц в 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":[{ ... ]};
          1. Илья Уткин 17 марта 2020, 15:29 # 0
            Хм, я с такой задачей не сталкивался, даже не знаю, в какую сторону «копать» ))
            1. Сергей 17 марта 2020, 16:05(Комментарий был изменён) # 0
              Дааа, я тоже впервые с этим сталкиваюсь! Проблема еще с рассинхроном с документацией по ExtJS 3.4. Нашел там метод reconfigure(), который, по сути, решает мою задачу, но в версии modX его почему-то нет, хотя написано, что работает с версии 2.3. Если хватит терпения и я разберусь в этом — напишу сюда для истории!
              1. unreal_serg 26 марта 2020, 02:18 # +1
                В общем, действительно, нужно применять метод reconfigure. Просто я его применял не туда!!!
                Написал пример на codepen. Там закомментированный код именно это и делает!
            2. Arty 24 декабря 2020, 13:57(Комментарий был изменён) # 0
              Подскажите, пожалуйста, как добавить свой стиль для столбца weight в таблице заказов minishop2 в orders.grid.js, например color: #eee;

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

              Авторизация

              через сервис Loginza:


              Шаблоны MODX

              1 2 Дальше »

              Объектная
              модель
              MODX