Jump to main content Jump to doc navigation

В принципе, всё необходимое для написания приличного дополнения к MODX я уже рассказал.

Мы знаем структуру компонента, умеем собирать его в пакет, управляем контроллерами и меняем интерфейс. Даже немного научились работать с GitHub. Дело за малым — собственно написать функционал.

Этот урок очень объёмный, здесь много листингов кода, с пояснениями. Если что-то непонятно — не нужно переживать и расстраиваться, просто помните, что всегда можно посмотреть исходный код уже готовых дополнений и самого MODX — там есть примеры на все случаи жизни.

Итак, сегодня нам нужно улучшить в админке таблицу и всплывающее окно для создния подписки. Вот, что у нас получится в итоге

Таблица

Таблица ExtJS с подписками у нас находится в файле /assets/components/sendex/js/mgr/widgets/newsletters.grid.js. Для работы с виджетом (компонентом) ExtJS его нужно объявить и зарегистрировать.

Коротенькое объявление

// Задаем переменную в объекте Sendex, которая содержит функцию
Sendex.grid.Newsletters = function(config) {
    // Вызов конструктора виджета, с переданными параметрами
    Sendex.grid.Newsletters.superclass.constructor.call(this,config);
};
// Наш виджет расширяет объект MODx.grid.Grid
Ext.extend(Sendex.grid.Newsletters,MODx.grid.Grid, {/* Здесь можно добавить или переписать методы расширяемого объекта*/});

Это просто пример для понимания приципа. При реальной работе нужно еще задать разных параметров таблице.

А теперь регистрируем наш виджет:

Ext.reg('sendex-grid-newsletters',Sendex.grid.Newsletters);

И вот теперь можно указывать xtype: 'sendex-grid-newsletters' где угодно — и там выведется наша таблица.

Правда, при реальной эксплуатации нам нужно еще:

  1. Добавить параметров в конфигурацию при инициализации объекта
  2. Добавить свои функции в таблицу, при расширении родительского объекта MODx.grid.Grid

Параметры таблицы

Основные параметры таблицы ExtJS:

  • id — идентификатор элемента. К нему потом можно обращаться как Ext.getCmp('идентификатор');
  • url — адрес для запроса данных с сервера, это почт ивсегда наш connector.php в assets
  • baseParams — параметры, передаваемые при запросе данных от сервера
  • fields — JSON массив полей, которые могут быть в ответе от сервера.
  • paging — включить пагинацию результатов
  • pageLimit — количество результатов на одной странице
  • remoteSort — сортировать результаты на сервере
  • columns — JSON массив со столбцами таблицы, и их свойствами
  • tbar — верхняя панель таблицы, обычно там поиск и кнопочки
  • listeners — JSON массив с функциями, которые будут выполняться при разных действиях с таблицей

Некоторые параметры не существуют в обычных таблицах ExtJS и добавлены только в MODX.grid — вот документация.

Нам нужно обязательно указать ключи нашего объекта sxNewsletter:

,fields: ['id','name','description','active','template','snippet','image','email_subject','email_from','email_from_name','email_reply']И колонки таблицы
,columns: [
    {header: _('sendex_newsletter_id'),dataIndex: 'id',width: 50}
    ,{header: _('sendex_newsletter_name'),dataIndex: 'name',width: 100}
    //,{header: _('sendex_newsletter_description'),dataIndex: 'description',width: 250}
    ,{header: _('sendex_newsletter_active'),dataIndex: 'active',width: 75,renderer: this.renderBoolean}
    ,{header: _('sendex_newsletter_template'),dataIndex: 'template',width: 75}
    ,{header: _('sendex_newsletter_snippet'),dataIndex: 'snippet',width: 75}
    ,{header: _('sendex_newsletter_email_subject'),dataIndex: 'description',width: 100}
    ,{header: _('sendex_newsletter_email_from'),dataIndex: 'email_from',width: 100}
    //,{header: _('sendex_newsletter_email_from_name'),dataIndex: 'email_from_name',width: 100}
    //,{header: _('sendex_newsletter_email_reply'),dataIndex: 'email_reply',width: 100}
    ,{header: _('sendex_newsletter_image'),dataIndex: 'image',width: 75,renderer: this.renderImage}
]

Я прописал все колонки, но некоторые сразу закомментировал — чтобы место не занимали.

Свойства колонки:

  • header — Заголовок, обычно используется запись из лексикона
  • dataIndex — Ключ массива field. То есть из какого места брать данные для вывода?
  • width — ширина
  • editor — можно указать массив для редактирования колонки прямо в таблице, но мы это пока не трогаем
  • renderer — Метод отображения колонки.

Очень интересен параметр renderer, в нём мы можем указать любую javascript функцию, которая будет готовить внешний вид данных перед отображением. Я использую его для двух столбцов: active и image.

Сами функции нужно указывать при расширении объекта:

Ext.extend(Sendex.grid.Newsletters,MODx.grid.Grid,{
    windows: {}
    // ...
    ,renderBoolean: function(val,cell,row) {
        return val == '' || val == 0
            ? '<span style="color:red">' + _('no') + '<span>'
            : '<span style="color:green">' + _('yes') + '<span>';
    }

    ,renderImage: function(val,cell,row) {
        return val != ''
            ? '<img src="' + val + '" alt="" height="50" />'
            : '';
    }Там же рядом, кстати, есть встроенная функция для вывод контекстного меню:
,getMenu: function() {
    var m = [];
    m.push({
        text: _('sendex_newsletter_update')
        ,handler: this.updateItem
    });
    m.push('-');
    m.push({
        text: _('sendex_newsletter_remove')
        ,handler: this.removeItem
    });
    this.addContextMenuItem(m);
}

Она просто добавляет элементы в массив и передаёт его в метод родительского объекта addContextMenuItem(). Если мы захотим изменить контекстное меню строки таблицы — редактировать нужно тут.

Добавляем новые записи в лексиконы, синхронизируем, чистим кэш и обновляем страницу в админке:

Наша таблица готова для работы, вот коммит со всеми изменениями.

Окошки

Не знаю, какие окошки у родного ExtJS, но вот у расширенного MODx.Window они просто замечательные. Это выражается в том, что они сразу соединены с формой и умеют работать через ajax.

То есть, при вызове окошка вы сразу получаете форму с кнопочками, и указываете параметрами, куда отправлять данные при сохранении, и что делать при ответе с сервера — вот документация.

Основные параметры окон:

  • title — заголовок окошка, обычно используется запись из лексикона
  • id — идентификатор виджета
  • height — высота
  • width — ширина
  • url — адрес для запросов на сервер, обычно это connector.php в assets
  • action — имя действия, обычно там указывается конкретный процессор
  • fields — массив с полями формы

Выходит, заполнить нужно всего один параметр fields:

,fields: [
    {xtype: 'textfield',fieldLabel: _('name'),name: 'name',id: 'sendex-'+this.ident+'-name',anchor: '99%'}
    ,{xtype: 'textarea',fieldLabel: _('description'),name: 'description',id: 'sendex-'+this.ident+'-description',height: 150,anchor: '99%'}
]

У нас два окошка: создание и обновление записи. Это переменные Sendex.window.CreateItem и Sendex.window.UpadteItem. Чтобы не было путанницы, предлагаю сразу все эти Item переименовать в Newletter.

Окошки у нас работаю еще с прошлого занятия, но нужно бы добавить в них полей для редактирования:

,fields: [
    {xtype: 'textfield',fieldLabel: _('name'),name: 'name',id: 'sendex-'+this.ident+'-name',anchor: '99%'}
    ,{xtype: 'numberfield',fieldLabel: _('sendex_newsletter_template'),name: 'template',id: 'sendex-'+this.ident+'-template',anchor: '99%'}
    ,{xtype: 'numberfield',fieldLabel: _('sendex_newsletter_snippet'),name: 'snippet',id: 'sendex-'+this.ident+'-snippet',anchor: '99%'}
    ,{xtype: 'textarea',fieldLabel: _('description'),name: 'description',id: 'sendex-'+this.ident+'-description',height: 150,anchor: '99%'}

    ,{xtype: 'textfield',fieldLabel: _('sendex_newsletter_email_subject'),name: 'email_subject',id: 'sendex-'+this.ident+'-email_subject',anchor: '99%'}
    ,{xtype: 'textfield',fieldLabel: _('sendex_newsletter_email_from'),name: 'email_from',id: 'sendex-'+this.ident+'-email_from',anchor: '99%'}
    ,{xtype: 'textfield',fieldLabel: _('sendex_newsletter_email_from_name'),name: 'email_from_name',id: 'sendex-'+this.ident+'-email_from_name',anchor: '99%'}
    ,{xtype: 'textfield',fieldLabel: _('sendex_newsletter_email_reply'),name: 'email_reply',id: 'sendex-'+this.ident+'-email_reply',anchor: '99%'}

    ,{xtype: 'combo-boolean',fieldLabel: _('sendex_newsletter_active'),name: 'active',hiddenName: 'active',id: 'sendex-'+this.ident+'-active',anchor: '50%'}
    ,{xtype: 'textfield',fieldLabel: _('sendex_newsletter_image'),name: 'image',id: 'sendex-'+this.ident+'-image',anchor: '99%'}
]

Основные параметры полей формы:

  • xtype — фиджет поля. Может быть встроенный: textfield, numberfield, textarea, checkbox или любой кастомный
  • fieldLabel — заголовок поля, обычно используется запись из лексикона
  • name — имя поля, именно оно будет ключом в массиве $_POST при отправке на сервер
  • id — идентификатор элемента
  • anchor — ширина относительно окна
  • style — можно добавить особое оформление полю ввода, например stye:'border:1px solid red;'

Синхронизируем изменения с сервером, чистим кэш, обновляем страницу и у нас все прекрасно работает! Только окошко с трудом влезает в экран.

Нужно разбивать форму на 2 столбца. Делается это вложенными массивами в fields:

,fields: [
    {xtype: 'textfield',fieldLabel: _('name'),name: 'name',id: 'sendex-'+this.ident+'-name',anchor: '99%'}
    ,{
        layout:'column'
        ,border: false
        ,anchor: '100%'
        ,items: [{
            columnWidth: .5
            ,layout: 'form'
            ,defaults: { msgTarget: 'under' }
            ,border:false
            ,items: [
                {xtype: 'modx-combo-template',fieldLabel: _('sendex_newsletter_template'),name: 'template',id: 'sendex-'+this.ident+'-template',anchor: '99%'}
                ,{xtype: 'textfield',fieldLabel: _('sendex_newsletter_email_subject'),name: 'email_subject',id: 'sendex-'+this.ident+'-email_subject',anchor: '99%'}
                ,{xtype: 'textfield',fieldLabel: _('sendex_newsletter_email_reply'),name: 'email_reply',id: 'sendex-'+this.ident+'-email_reply',anchor: '99%'}
                ,{xtype: 'combo-boolean',fieldLabel: _('sendex_newsletter_active'),name: 'active',hiddenName: 'active',id: 'sendex-'+this.ident+'-active',anchor: '50%'}
            ]
        },{
            columnWidth: .5
            ,layout: 'form'
            ,defaults: { msgTarget: 'under' }
            ,border:false
            ,items: [
                {xtype: 'sendex-combo-snippet',fieldLabel: _('sendex_newsletter_snippet'),name: 'snippet',id: 'sendex-'+this.ident+'-snippet',anchor: '99%'}
                ,{xtype: 'textfield',fieldLabel: _('sendex_newsletter_email_from'),name: 'email_from',id: 'sendex-'+this.ident+'-email_from',anchor: '99%'}
                ,{xtype: 'textfield',fieldLabel: _('sendex_newsletter_email_from_name'),name: 'email_from_name',id: 'sendex-'+this.ident+'-email_from_name',anchor: '99%'}
                ,{xtype: 'modx-combo-browser',fieldLabel: _('sendex_newsletter_image'),name: 'image',id: 'sendex-'+this.ident+'-image',anchor: '99%'}
            ]
        }]
    }
    ,{xtype: 'textarea',fieldLabel: _('description'),name: 'description',id: 'sendex-'+this.ident+'-description',height: 75,anchor: '99%'}
]

Как видите, вместо второго поля у нас массив, в котором элемент с указанием разметки layout: columns — это специальная магия ExtJS. А у этого элемента 2 подмассива с разметкой layout: form и с указанием ширины колонки columnWidth: .5.

Вот так непросто делаются колонки в форме. Это нужно просто запомнить и копипастить при необходимости.

Собственные поля формы

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

Для выбора картинки указываем готовый xtype: 'modx-combo-browser', для шаблона xtype: 'modx-combo-template', а вот для сниппета ничего готового нет. Откуда я знаю, что можно использовать? Очень просто — я смотрю в исходники MODX.

Нужно написать свой xtype для вывода сниппетов. Для этого лучше создать /assets/components/sendex/js/mgr/misc/sendex.combo.js и сразу подключить его в контроллере `home.

$this->addJavascript($this->Sendex->config['jsUrl'] . 'mgr/misc/sendex.combo.js');

Регистрируем новый xtype sendex-combo-snippet в файле misc/sendex.combo.js:

Sendex.combo.Snippet = function(config) {
    config = config || {};
    Ext.applyIf(config,{
        name: 'snippet'
        ,hiddenName: 'snippet'
        ,displayField: 'name'
        ,valueField: 'id'
        ,fields: ['id','name']
        ,pageSize: 10
        ,hideMode: 'offsets'
        ,url: MODx.config.connectors_url + 'element/snippet.php'
        ,baseParams: {
            action: 'getlist'
        }
    });
    Sendex.combo.Snippet.superclass.constructor.call(this,config);
};
Ext.extend(Sendex.combo.Snippet,MODx.combo.ComboBox);
Ext.reg('sendex-combo-snippet',Sendex.combo.Snippet);

Очень похоже на регистрацию таблицы, не так ли? Конечно так, но есть несколько отличий в параметрах:

  • Я использую родной процессор MODX для получения имеющихся сниппетов, поэтому такой необычный путь к процессору
  • Параметры displayField и valueField указывают, какой ключ из fields нужно отображать, а какой считать значением и отправлять в $_POST
  • Параметр name и hiddenName имя поля, и имя комбобокса. Обычно они должны совпадать, чтобы все правильно работало.

Теперь указываем новый xtype в поле формы и проверяем:

Обработка формы в процессоре

Согласно параметру url формы, все запросы у нас уходят на основной коннектор Sendex в директории assets. А вот действие мы передаём mgr/newsletter/create — этот процессор нам и нужен.

Редактируем /core/components/sendex/processors/mgr/newsletter/create.class.php

public function beforeSet() {

    $required = array('name', 'template');
    foreach ($required as $tmp) {
        if (!$this->getProperty($tmp)) {
            $this->addFieldError($tmp, $this->modx->lexicon('field_required'));
        }
    }

    if ($this->hasErrors()) {
        return false;
    }

    $unique = array('name');
    foreach ($unique as $tmp) {
        if ($this->modx->getCount($this->classKey, array('name' => $this->getProperty($tmp)))) {
            $this->addFieldError($tmp, $this->modx->lexicon('sendex_newsletter_err_ae'));
        }
    }

    $active = $this->getProperty('active');
    $this->setProperty('active', !empty($active));

    return !$this->hasErrors();
}

Как видите, я добавил проверку на заполнение полей name и template. Если они не пусты, то проверяю на уникальность имени. Если есть ошибки — мы увидим вот такой ответ:

А если ошибок нет, то форма сохранится и в таблице появится новая строка.

Также в конце метода есть приведение типа поля active, потому что форма шлёт или строку 'true' или пустоту. Поэтому превращаем его в булево, чтобы подходило к нашей модели.

Вызов окошек из таблицы

Ну и напоследок нужно понять, а как же именно вызываются окошки при работе с таблицей?

Окошко — это перемнная с функцией, например Sendex.window.CreateNewsletter. Чтобы показать его, мы должны сделать кнопку и повесить на неё обработчик, что и сделано в параметре tbar таблицы:

,tbar: [{
    text: _('sendex_btn_create')
    ,handler: this.createNewsletter
    ,scope: this
]}

Обработчик вызывает метод createNewsletter из таблицы, смотрим на него:

,createNewsletter: function(btn,e) {
    if (!this.windows.createNewsletter) {
        this.windows.createNewsletter = MODx.load({
            xtype: 'sendex-window-newsletter-create'
            ,listeners: {
                'success': {fn:function() { this.refresh(); },scope:this}
            }
        });
    }
    this.windows.createNewsletter.fp.getForm().reset();
    this.windows.createNewsletter.show(e.target);
}
  • Окно создаётся один раз при помощи метода MODx.load() в котором указывается, что именно грузить. В данном случае xtype окошка создания.

  • При загрузке xtype передаётся массив listeners с указанием функций для событий. В частности, при success, то есть положительном ответе от сервера будет обновление таблицы.

  • Все поля формы очищаются — это сделано для повторных вызовов, когда окно уже загружено и может хранить значения в форме.

  • Окно показывается на экран.

  • Вот коммит со всеми изменения по окошку новой подписки.

Заключение

Скорее всего, сегодняшний урок покажется большинству читателей довольно сложным, но это не так. Запомните главное правило разработчика:

Если чего-то не понимаешь — смотри как делают другие

То есть, если что-т онепонятно — смотрите исходный код моих дополнений и самого MODX. Там очень много примеров, хотя бы вот файл с комбобоксами от miniShop2 — практически готовая библиотека для выбора чанков, ресурсов, юзеров и т.д.

Ну и конечно не помешает документация по Ext.grid.GridPanel, по Ext.Window и по Ext.form.Combo. Правда, нужно дойти до определённого уровня понимания предмета, чтобы она начала приносить пользу. Лично у меня это произошло далеко не сразу.

На следующем занятии мы будем делать окошко с редактированием подписки и назначать ей юзеров.

Узнать больше

Support the team building MODX with a monthly donation.

The budget raised through OpenCollective is transparent, including payouts, and any contributor can apply to be paid for their work on MODX.

Backers

  • modmore
  • STERC
  • Jens Wittmann – Gestaltung & Entwicklung
  • Fabian Christen
  • Digital Penguin
  • Dannevang Digital
  • Sepia River Studios
  • CrewMark
  • Chris Fickling
  • deJaya
  • Following Sea
  • Anton Tarasov
  • eydolan
  • Raffy
  • Lefthandmedia
  • Murray Wood
  • Snow Creative
  • Nick Clark
  • Helen
  • JT Skaggs
  • krisznet
  • YJ
  • Yanni
  • Richard

Budget

$366 per month—let's make that $500!

Learn more