Что такое плагин


Плагин (подключаемый модуль) это класс, который может быть добавлен в конфигурацию какого-нибудь компонента (свойство
plugins
). Все плагины должны иметь метод
init()
, который вызовет клиентский компонент, передав себя в качестве единственного параметра, в самом начале инициализации компонента, до его отрисовки.

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

Обычно это делается путем описания каких-то методов, вызываемых до или после базовых методов компонента.

Способы расширения функциональности


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

Например, у Ext.form.Field есть свойство
fieldLabel
, которое используется в контейнере формы. Допустим, мы хотим избавиться от необходимости конфигурировать это свойство и позволить полям формы самостоятельно указывать свои названия. Для этого у нас есть как минимум три варианта:

1. Конфигурация компонента


Разработчик может использовать события компонента, и до его отрисовки назначать названия полей:

{
    xtype: 'textfield',
    name: 'name',
    listeners: {
        beforerender: function(field) {
            field.fieldLabel = field.name; // Название поля будет аналогичным его имени
        }
    }
}


В случае, если это разовая необходимость, то это может быть самым простым вариантом, но как только возникает необходимость повторить это с другим полем, начинается дублирование кода (а мы с вами знаем - это плохо).

Еще одним недостатком является то, что события могут быть отменены программно - например, в каком-то другом обработчике. Мы не можем полагаться на события.

2. Создание подкласса


Разработчик может создать подкласс и переопределить шаблонные методы (о которых чуть ниже), которые обеспечивают структуру и жизненный цикл компонента. Родительский метод суперкласса, при этом, вызывается до или после соответствующего метода подкласса. Пример возможной реализации (см jsfiddle):

// Создаем подкласс
Ext.define('customField', {
    extend: 'Ext.form.field.Text', // суперкласс
    alias: 'widget.mytextfield', // псевдоним - теперь xtype = 'mytextfield'
        
    onAdded: function() { // шаблонный метод
        this.callParent(arguments); // вызываем метод суперкласса
        this.fieldLabel = this.name; // назначаем fieldLabel
    }
});
 
// Применяем наш подкласс
...
{
    xtype: 'mytextfield', // subclass
    name: 'name'
}
...


Такой подход - неплохой вариант, однако и он не лишен недостатков. На форме может быть множество типов полей, не только текстовые - еще есть выпадающие списки, поля загрузки файлов и т.д (в Ext JS все они являются потомками Ext.form.field.Base). Создав подкласс, мы можем расширить только какой-то определенный тип (в данном случае textfield), но не можем охватить всю иерархию.

3. Создание плагина


Этот вариант обеспечивает наибольшую гибкость с точки зрения повторного использования кода. Грамотно написанный плагин, созданный для одного класса, может быть применен ко всем его подклассам. Кроме того, вы можете применить к компоненту несколько плагинов (очевидно, что при этом плагины не должны конфликтовать друг с другом).

Реализация плагина


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

Лучше избегать событий, потому что, как уже говорилось выше, они могут быть отменены программно, перехвачены другими слушателями, нет никакой гарантии когда на него отреагирует наш плагин - до или после других обработчиков.

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

Шаблонный метод - Template Method Pattern


Иерархия классов Ext JS использует паттерн Шаблонный метод, делегируя на подклассы поведение, характерное для этого конкретного подкласса.

Смысл этого заключается в том, что каждый класс в цепочке наследования, может "прицепить" лишний кусок логики на определенных этапах жизненного цикла компонента. На каждом этапе, класс реализует свое особое поведение, позволяя другим классам в цепочке наследования реализовать свою логику. Использование шаблонных методов похоже на использование событий - они обрабатываются в определенные моменты жизненного цикла компонента.

Примером может служить метод
render
. Этот метод реализован в суперклассе всех компонентов - AbstractComponent, и отвечает за фазу отрисовки компонента. Во время работы он вызывает метод
onRender()
, позволяя своим подклассам переопределить его, реализовав свою специфическую логику. Каждый переопределенный
onRender()
в подклассах должен вызвать
onRender()
суперкласса до реализации своей дополнительной логики.

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

Шаблонные методы, которые вы можете использовать при реализации подклассов в Ext JS 4:

  • initComponent
    - Этот метод вызывается конструктором. Он используется для инициализации данных, конфигурирования, назначения обработчиков событий.

  • beforeShow
    - Вызывается перед показом компонента.

  • onShow
    - Позволяет дополнительно описать поведение при показе компонента. После вызова
    onShow()
    суперкласса, компонент станет виден.

  • afterShow
    - Этот метод вызывается после того, как компонент был показан.

  • onShowComplete
    - Вызывается сразу после завершения
    afterShow()
    .

  • onHide
    - Позволяет обработать операцию скрытия. После вызова
    onHide
    суперкласса, компонент будет скрыт.

  • afterHide
    - Вызывается после скрытия компонента.

  • onRender
    - Позволяет обработать операцию отрисовки.

  • afterRender
    - Вызывается после отрисовки компонента. На этом этапе элемент компонента будет стилизован в соответствии с конфигурацией, будут добавлены имена CSS классов, сконфигурирована "включенность" или "выключенность" компонента.

  • onEnable
    - Позволяет обработать операцию "включения". После вызова
    onEnable()
    суперкласса компонент будет "включен".

  • onDisable
    - Позволяет обработать операцию "выключения". После вызова
    onDisable()
    суперкласса компонент будет "выключен".

  • onAdded
    - Позволяет обработать операцию добавления компонента в контейнер. На этом этапе, компонент находится в коллекции дочерних элементов контейнера. После вызова
    onAdded
    суперкласса, будет определена ссылка в свойстве
    ownerCt
    , а если в конфигурации определено свойство
    ref
    , то будет установлено свойство
    refOwner
    .

  • onRemoved
    - Позволяет обработать операцию удаления компонента из родительского контейнера. На этом этапе компонент удален из коллекции дочерних элементов родительского контейнера, но не был уничтожен (он будет уничтожен если свойство
    autoDestroy
    родительского контейнера установлено в
    true
    , или если удаление было явно вызвано вторым параметром). После вызова
    onRemoved()
    суперкласса, ссылки в
    ownerCt
    и
    refOwner
    перестанут существовать.

  • onResize
    - Позволяет обработать изменение размеров компонента

  • onPosition
    - Позволяет обработать позиционирование компонента.

  • onDestroy
    - Позволяет обработать уничтожение компонента. После вызова
    onDestroy()
    суперкласса, компонент будет уничтожен.

  • beforeDestroy
    - Вызывается перед уничтожением компонента.

  • afterSetPosition
    - Вызывается после того, как компонент был спозиционирован.

  • afterComponentLayout
    - Вызывается после подготовки макета компонента.

  • beforeComponentLayout
    - Вызывается перед подготовкой макета компонента.


Многие компоненты имеют специфические шаблонные методы, которые тоже можно использовать.

Хуки в шаблонном методе


Плагин должен добавить свою логику до или после шаблонного метода, выполняемого клиентским компонентом. Чтобы это реализовать, мы должны понять как создать перехватчики функций(interceptors) и функции-последовательности (sequences).

Перехватчики (interceptors)


Перехватчик это функция, которую использует разработчик для расширения функциональности, вызывая ее перед выполнением уже существующего метода. В качестве примера, давайте расширим Ext.AbstractComponent так, что мы сможем выводить все события компонентов в консоль файрбага (их будет много!):

function fireEventInterceptor(evt) { // перехватчик
    var a = arguments;
    var msg = 'fired the following event "' + evt + '" with args:';
        
    console.log(msg);
    console.log(Array.prototype.slice.call(a, 1, a.length));
    return true;
}
    
var o = Ext.AbstractComponent.prototype;
// перехватываем метод fireEvent
o.fireEvent = Ext.Function.createInterceptor(o.fireEvent, fireEventInterceptor);


Вы можете посмотреть результат на jsfiddle (не забудьте открыть консоль).

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

Последовательности (sequences)


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

var o = Ext.AbstractComponent.prototype;
// назначаем fireEvent-у последовательность
o.fireEvent = Ext.Function.createSequence(o.fireEvent, fireEventSequence);


Второе отличие - она не может отменить вызов оригинальной функции (т.к. вызывается после нее).

Плагин fieldLabeler


Итак, плагин это синглтон. Мы не можем вызвать его непосредственно, но можем подключить его к любому компоненту, просто прописав:

plugins: [ Ext.ux.FieldLabeler ]


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

При решении нашей задачи путем написания плагина, вместо описания подкласса и переопределения методов родителя, мы устранили серьезную проблему - наш плагин можно подключить к любому подклассу Ext.form.field.Base: TriggerField, TextArea, NumberField и т.д.

Пара советов - наследуйте ваши плагины от Ext.AbstractPlugin, тогда для него будут доступны методы
enable()
,
disable()
, и прочие, которые вы можете увидеть в документации по Ext.AbstractPlugin.
Указывайте плагину свойство pluginId, тогда его можно будет получить в компоненте, вызвав
component.getPlugin('plugin_id')
.

Итак, опишем наш плагин, применив полученные знания:



It's working!

Это вольный перевод статьи в блоге Sencha "Advanced Plugin Development with Ext JS". Содержание было исправлено и дополнено, т.к. оригинальная статья писалась во времена Ext JS 3 и немного устарела.