MODX - MODExt

Введение


MODXExt - как можно догадаться это расширение extJS. На нем построены все встроенные виджеты MODX Revo. Как написано в документации, JS фрейворков полно, но только два из них - обепечивают полноценные UI(интерфейс пользователя). И это Yahoo User Interface и Ext JS. Так что проникнитесь). Если ранее не имели дело с ExtJS, то приготовьтесь испытать шок и трепет. Ну очень сложный и навороченный. Зато возможностей в нем тьма тьмущая. 

Надо заметить, что и ExtJs и его потомок MODExt - тесно связаны со скриптами на сервере. Это коннекторы, котроллеры, процессоры и прочее. Поэтому придется затронуть и серверную часть. Многие управляющие элементы в виджетах имеют в настройках(конфигурациях) параметры для ajax. Это таблицы(grid), комбобоксы, формы и даже различного рода MessageBox, коих тут с десяток. 

MODExt


В админке для виджетов уже есть много компонентов MODExt. Полный перечень можно глянуть тут - /manager/assets/modext/widgets/core/

В систему уже загружены объекты

  • Ext - это объект extJS
  • MODx - объект MODExt

Причем в MODx содержится вся конфигурация. Текущее id ресурса, параметры запроса.

У объектов extJS есть не только свойства и методы, но и конфигурация. Используется при инициализации. Это подобно как если в конструктор в качестве параметра передать ассоциативный массив. Весьма удобно и прозрачно. 

 В конструкторе создаваемых компонентов рекомендуется исползовать Ext.applyIf. Пример :

Ext.applyIf( config, {

        id : "mycomponent-grid-mygrid",
        title : "Сборы",
Т.е. если в конфиге нет каких либо параметров, они будут инициализированы значениями по умолчанию. При этом часто эти значения по умолчанию задаются отдельным объектом. 
Кастомные виджеты (свои типы выводов, приложения и т.п) принято располагать в "/assets/components/"( ну а серверная часть, то что на php, в "/core/components/")

xtype


xtype -  это объекты ExtJS или его потомка MODExt. Это не то же что объекты JS. Удобная штука. Можно создать свой компонент. Что угодно - набор полей, кнопку, grid и т.д. потом зарегистрировать его в качестве xtype:

    Ext.reg( "mycomponent-grid-mygrid", MyComponent.grid.MyGrid );

и можно включать его в другие компоненты:

  tpanel : [ {
       xtype : "mycomponent-grid-mygrid",
  ............

Т.е. xtype можно рассматривать просто как куски кода. которые удобно вставлять в нужное место.  В системе есть много готовых xtype. Они обычно прошиты в компонентах. Например xtype:'modx-combo' можно найти в "manager/assets/modext/widgets/core/modx.combo.js". 

 И на момент использования - xtype должен быть загружен, напр. так ->  MODx.load({ xtype: 'mycomponent-grid-mygrid'});

Порядок подключения виджетов


Далее все на примере виджета сдачи показаний счетчиков(ИПУ).

Оптимальный порядок подключения виджетов следующий:

var Counters = function(config) { 
config = config || {};
Counters.superclass.constructor.call(this,config);
}; - конструктор класса 
Ext.extend(Counters,Ext.Component,{
page:{},window:{},grid:{},tree:{},panel:{},combo:{},config: {} - это члены(свойства) класса Counters, Ext.Component - предок
}); - наследование, устанавливает предка(создает его экземпляр, иначе нельзя унаследовать в JS). Реально не создает экземпляр класса
Ext.reg('counters',Counters); - теперь доступен как xtype 'counters'. Но реально он тут не создается. А создается ниже.
Counters = new Counters(); - реальное создание экземпляра класса. Создаем глобальный объект extJS. По сути объект JS с наворотами.

 Таким образом у нас есть глобальный объект (singleton), содержащий заготовки дочерних объектов(page:{},window:{},grid:{},tree:{},panel:{},combo:{} . . . ). И далее создаются эти дочерние объекты по следующему алгоритму.

  • создается конструктор
  • назначается ему предок
  • регистрируется как xtype
  • загружается созданный xtype, при этом создается экземляр класса(вызывется конструктор, который вызывает конструктор предка и т.д.)

 Таким образом, реально объект можно создать двумя путями:

  1. Классически, через оператор new - пример: Counters = new Counters(); 
  2. через  MODx.load({ xtype: 'counters-page-home'});
  3. есть еще удобный неявный вызов  MODx.load через конфигурации  - tpanel : [ { xtype : "mycomponent-grid-mygrid", .......

 Пример загрузки(через  MODx.load): 

Ext.onReady(function() {
    MODx.load({ xtype: 'counters-page-home'});
});
Counters.page.Home = function(config) {
          config = config || {};
          Ext.applyIf(config,{
          components: [{
               xtype: 'counters-panel-home'
               ,renderTo: 'counters-panel-home-div'
          }]
     });
     Counters.page.Home.superclass.constructor.call(this,config);
};

Ext.extend(Counters.page.Home,MODx.Component);
Ext.reg('counters-page-home',Counters.page.Home);

Так положено. У некоторых может возникнуть вопрос - и это единственный способ? Ответ нет. Более того, по моему, не самый лучший. Например, чтоб установить на страницу панель, можно просто создать стандартную(предопределенную) и дополнить ее своими параметрами. Например так:

var panel = new Ext.Panel({ title : 'Моя панель', width : 600, height: 300, html : ' Контент Контент Контент Контент Контент ', renderTo : 'test' });

Допустим надо вставить кнопку:

var button = new Ext.Button({
text: 'Моя кнопка'
}); - создали кнопку и установим ее в панель. Для наглядности установим наряду с xtype:

var panel = new Ext.Panel({
title : 'Моя панель',
width : 600,
height: 300,
renderTo : 'div_to_render',
layout : 'border',
defaults : {
padding: '3'
},
items : [{
xtype : 'panel',
region: 'west',
title : 'Вложенная первая',
html : 'контент контент контент ',
tbar: [
button,
{ xtype: 'button', text: 'Button 1' }
]
},{ ................

Причем код получился намного короче. Надо помнить, что подключаемый xtype, по сути, делает то-же самое. В смысле - создает элемент. Мораль - не всегда есть смысл городить кастомные xtype. В большинстве случаев вообще можно обойтись без создания своих xtype.  

Даже если отделить конфигурацию, код все равно часто выглядит громозко. Чтобы как-то разгрузить мозг можно разделить сам конфиг, отдельно задавать, допустим, события. Особенно, когда они повторяюся. Пример:

var chEv = {'change':{fn:function(){Ext.getCmp('panel_23').markDirty();},scope:this}};

Далее в конфиге:
     ...............
    ,border: false
    ,items: [{
        xtype: 'textfield'
        ,fieldLabel: 'Альбом'
        ,description: '{/literal}{$gl.parent_desc}{literal}'
        ,name: 'inopt_album'
        ,id: 'inopt_album{/literal}{$tv}{literal}'
        ,value: params['album'] || ''
        ,width: 300
        ,listeners: chEv
    },{
        xtype: 'combo-boolean'
     ...............

  

 

 

 

Поиск элементов


Манипуляции с выборкой:

var els =Ext.select("#some-el div.some-class");
// or select directly from an existing elementvar
el =Ext.get('some-el'); el.select('div.some-class'); els.setWidth(100);// all elements become 100 width els.hide(true);// all elements fade out and hide// or els.setWidth(100).hide(true);

 установить значения полей проще на чистом js:

document.getElementById('tv-tsv-color').value="Красный";

Но иногда нужно, что срабатывало событие "change". Если вставить из буфера, или просто настукать - оно срабатывает. А если установить из кода - нет. В таком случае лучше сделать так:

var el=Ext.getCmp('tv-tsv-color');
el.setValue('Красный');
el.fireEvent('change', el, 'Красный', 'Голубой');

Ext.Element - это "обертка" для HTML (DOM) элементов. Обегчает манипуляцию и прочее.

Ext.Component - это уже чисто объкты ExtJS. Таблицы, панели и прочее. Они имеют конфигураторы для конструктора, свойства и методы. 

 

функция возврат, что делает пример
getEl( ) Ext.Element. в его же конструкторе, напр. в обработчиках ...listeners: { render: function(p) { p.getEl...
get( el ) Ext.Element, краткое от Ext.Element.get, аргумент: id, или DOM Node, или Ext.element Ext.get('body')
getCmp( id ) Ext.Component, краткое от Ext.ComponentMgr.get getCmp('grid1234')
query( path, [root] ) Array, краткое от Ext.DomQuery.select, поиск CSS селектору и возврат массива Dom элементов Ext.pluck(Ext.query(
select( selector, [root] ) CompositeElement or CompositeElementLite, в остальном - аналог query var els = Ext.select("#some-el div.some-class");

Плюсы и минусы MODExt


ПЛЮСЫ:

  • Процессоры заточены под MODExt. Поэтому требуют минимум кода. буквально пара строк и получаем полный хендлер
  • Упрощенное неявное подключение STORE таблиц. Параметры коннекта и прочее указываются в конфигураторе таблицы.
  • Улучшенные message box. С встроенными интерфейсами для ajaх запросов и прочим.
  • Удобные контекстные меню в таблицах. Т.е. нет нужды генерить action - столбцы

МИНУСЫ:

  • "Дефектные" getList процессоры. Даже если назначить выборку отднльных полей - все равно выберет все и отправит в броузер. По дефолту включена разбивка на страницы и выбирает по 20 строк, что реально выбешивает по началу, поскольку это не документировано. Даже явное указание в процессоре не помогает. Есть хуки для коррекции SQL перед передачей в xPDO. Но xPDO живет своей жизнью и игнорирует многие вещи.
  • Перекрытие некоторых событий и обработчиков ExtJs, что и явилось причиной того, что я полностью отказался от MODExt в части таблиц.
  • Поскольку все процессоры заточены под MODExt надо быть очень осторожным при испльзовании читого ExtJs, поскольку добавлены параметры, отсутствующие в последнем.
  • Документация так скажем  не то что хреновая, ее практически вообще нет. Все время приходится ковырятся в исходниках.  Хотя в последнее время начинаеи появляться(https://docs.modx.com/current/en/extending-modx/custom-manager-pages/modext). 

Вот такие пироги. Но если надо просто ввести таблицу, с нехитрым редактированием - пойдет. Так то вся админка построена на MODExt.

Заморочки ExtJs


ExtJs крут немерянно, в силу этого у него много странностей.
- store грида надо подключать до его рендеринга, иначе в консоли выскочит трудная в понимании ошибка. Типа "а.add not function".

К слову сказать, ExtJs очень непрост в отладке в силу подобных сообщений об ошибках.

При работе с DOM я страюсь работать на чистом JS. Иначе путаюсь с сущностями - толи это компонент, толи элемент, толи просто объект DOM. Наверное, к этому можно привыкнуть.

Аналогично с доступам к самим компанентам ExtJs из обработчиков и событий. Поэтому, пострадавши вдоволь, я взял за правило создать глобальный объект. Напр.: CPPVCFG={}. И все наиболее важные и нужные в будущем компоненты привязываю  к нему: CPPVCFG.mainGrid=new .........

И в любом обработчике не заморачиваюсь - CPPVCFG.mainGrid.refresh();

Хотя есть тьма геттеров для доступа из панели к нужному табу, из таба к гриду, из грида к бару, из бара к кнопке. Есть и прочие подобные фишки, например: ref. Но все это вызывает напряг мозга, а мозг надо беречь).

Есть так же заморочка с TabPanel. Если рендерить, нпример, таблицу как компонент в невидимый таб, то не получится. Видать не ренднерится в невидимые блоки. Может и не поэтому. Не стал особо копаться. Просто на время рендера делаю его активным - "CPPVSANAT.TAB.setActiveTab(1); " Тогда все ок. 

При рендере, например: contactForm.render('sch_h'), всегда смотрите чтоб это было в блоке Ext.onReady(()=> {........}). Как то сутки матерился и проклинал тот день когда взялся за ExtJs. Прав был Ницше - чтоб ни случилось, ищи причину сначала в себе. Хотя к моменту рендера все уже давно должно было грузануться. Ну и блок-контейнер уже должен быть видимым. Ну это уж прозвучало. Но несколько раз уж напарывался в это. Например при рендере формы в окно бутстрапа. Причем то срабатывало, то нет. Все стало нормально когда рендер поместил в событие паказа окна

 $('#ModalSchet').on('shown.bs.modal', (e)=> {
 	     CFGCPPV.shetForm.render('sch_h');
 })

 

Что делать?


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

<?
class SonatMainGetListProcessor extends modProcessor {
           public function process() {
$debug=array();
$sql="SELECT pagetitle,id FROM {$this->modx->table_prefix}site_content
                WHERE template = 3 order by pagetitle";
              $debug['sql']=$sql;                
 $sth = $this->modx->prepare($sql);
 $sth->execute();
 
 $data = array(			// массив для возврата
    	'results'=>array(),
    	'total' => $sth->rowCount(),
    	'debug'=> $debug,
        );
while($row = $sth->fetch()){$data['results'][] = $row;}
return json_encode($data);
           }
} 
return 'SonatMainGetListProcessor';

Немного тут поясню нюансы. В классах и функциях к самому modx можно обратиться, объявив его через global. Но это, как правило,не нужно. Все классы имеют доступ через $this->modx. Особый геморой с таблицами. Они имеют префикс, который задается юзером при установке modx. Есть фукция $modx->getTableName(`site_tmplvar_contentvalues`), для получения реального имени. Но столкнулся с тем, что не везде она работает. Да и хрен с ней. Чтобы в процесоре получить этот префикс я в коннекторе его запоминаю - $modx->table_prefix=$table_prefix;

Для отладки можно добавить здесь дебужную инфу - 'debug'=> $debug, чего невозможно в стандартных процессорах. А потом глянуть а броузере:

 Но тут есть еще траблы при работе на чистом ExtJs.

1. Надо доработать коннектор.  В диспетчере запросов все параметры берутся из $_REQUEST.  Если нужно перенаправить, то делается примерно так: $_REQUEST['action']=$_POST['table'].'/'.$_POST['xaction'];   ExtJS автоматом генерит $_POST['xaction']. А MODX генерит 'action'. 

2. Поскольку процессоры заточены под MODX - придется несколько доработать процессор update для возможности редактировать прямо в таблице.

class SNomerUpdateProcessor extends modObjectUpdateProcessor {
    public $classKey = 'sanat35_nomer';
    public $languageTopics = array('counters:default');
    public $objectType = 'counters.city';
    //public $permission='CountersCityUpdate';
    public function initialize() {
        $data = $this->getProperty('results');
        if (empty($data)) return "Не обнаружен results";
        $data = $this->modx->fromJSON($data);
        if (empty($data)) return "Пустые данные";
        $this->setProperties($data);
        $this->unsetProperty('data');
        return parent::initialize();
    }
}
return 'SNomerUpdateProcessor';

Т.е. перекрыть метод "initialize()". Этот процессор ждет сразу масив данных. Между тем как ExtJs шлет их в виде литерального json. Поэтому перегоняем данные в массив и подменяем все параметры этим массивом.

И еще тут можно изменить входящие поля.

Дебугеры


Хотя у меня написан свой класс дебугера(трассера) для отладки на сервере, в последнее время все чаще применяю вариант привязанный к ExtJs. Но можно использовать и просто из релдактора js в Мозиле или в сниппетах Хрома.

Суть в добавлении к ответному json массива "debug".

Общая схема формирования ответа процессора такая(я так делаю):

  • $db=array();

    $db['$_POST']=$_POST;  - добавляем дебужину

    ...............................................................................

    $arrV=array();   - массив для ответа
    $arrV["kl_name"] =$h["client"][0];   -  заполняем данные
    $arrV["kl_company"] =$h["client"][0];
    $arrV["kl_phone"] =implode(';',$h['phone']);
    $arrV["kl_city"] =$h["city"][0];

    $db['$arrV']=$arrV;  - добавляем дебужину

    $data = array( // массив для возврата

    'results'=>$arrV,

    'total' => 1,  // 1- т.к. одна строка
    'debug'=> $db,
    );

    $outJson=json_encode($data,JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_NUMERIC_CHECK);  - формируем литерал
    die($outJson);  - возвращаем в броузер
    В результате в консоли можем просмотреть в очень удобном виде отладочную инфу. Как обычные переменные, так и массивы(как объекты js)

Работа с датами


С датами в PHP и mySQL есть некоторый геморой. Покажу на примере.

вот так выглядят поля с датами в modx. использован тип long. Согласен, что так можно избежать некоей путаницы. Но мне неудобно постоянно делать приведение, которое трудно угадать. один раз удалось, но не помню к какому типу

Поэтому свои поля делаю с типом timestamp. И хорошо отображается.

Но есть сложности при записи в базу. Выяснил, что для нормальной записи литерал должен быть в формате "2020-01-11 18:37:00", т.е. так же как и отображает mySql. А клиенты просят отображать в инпутах "11.01.2020" "18-37"  

Поэтому приходится делать так:

$date_ = date_create_from_format('d.m.Y H-i', $h['date_'][0]." ".$h['time_'][0]);
$arrV['date'] = date('Y-m-d H:i:s', $date_->getTimestamp() );

Тогда все ок.

Store из памяти, без подключения к серверу


Как то была задача подключить таблицу(grid) просто к json, потом взять измененные данные и далее с ними работать.

И тут завис надолго. Естественно разобрался, вот всплывшие нюансы:

  • не надо назначать writer. он сразу пытается связать store c сервером. И система ищет url.
  • не надо назначать proxy. Читал, что для типа ArrayStore подключается особый. Для работы с памятью. Но мне удобне Json.
  • в cfg store назначил json c данными - data:dataSch. 

Запустилось все, данные в grid редактировались. Я по наивности думал, что коли подключены данные, то и изменяться они там и должны. Был не прав.Потому как store.data - это уже отнюдь не наши данные и никакой связи с исходным json уже не имеют. store.data - это Ext.util.MixedCollection, что то  объект такой. А store.data.items - массив объектов Record. И вот store.data.items[0].data - содержит объект данных текущих. 

store.save() - наивно, т.к. эта штука только для отправки на сервер через writer. 

Причем в доках не нашел подобных зависимостей. Все из изучения объектов в консоли броузера. И в инете ничего не нашел. Все кусками.

При изменении появится красный маркер(как всгда). Для сброса маркера надо вызвать - store.commitChanges(); это сбросит у записи флаг dirty. Но и тут могут быть проблемы с отображение изменений. Я ж сделал так:

  • назначил обработчик  (провозился с ним часа 2):
 
listeners:{
update:( store, record, operation )=>{
 var key=Object.keys(record.modified)[0];
 CFGCPPV.sch.r[record.id][key]=record.data[key]; 
 record.dirty=false;
 record.modified=[];
		} 
}

Смыл сего - очистить флаг dirty, записать в исходный массив измененное поле, сбросить измененные поля. Еще обновил исходный json CFGCPPV.sch.r.  Чтобы появился record.id  при загрузки в Store добавил свойство id в каждую строку  равное номеру строки.

Для ArrayStore вроде все проще. Но некогда было эксперименировать. Да и удобнее тут было с json работать.

Ссылки


http://docs.sencha.com/extjs/3.4.0/ - родные доки по extJS

http://docs.sencha.com/extjs/3.4.0/#!/api  - api

https://docs.modx.com/revolution/2.x/developing-in-modx/advanced-development/custom-manager-pages/modext -  MODExt

http://www.w3ii.com/ru/extjs/extjs_quick_guide.html - краткое руководство на русском

https://metanit.com/web/extjs/ - на русском для версии 4

https://ilyaut.ru/extjs/ - уроки от Ильи Уткина


Комментарии 0






Разрешённые теги: <b><i><br>Добавить новый комментарий: