Рано или поздно наступает момент, когда возможностей фреймворка вам недостаточно. "Ах, как же так, забыли такую полезную кнопочку/контрол/приложение сделать..." - сетуете вы. Вот тогда и приходит в голову идея написать нехватающий функционал самому. Точнее дописать. И любой уважающий себя фреймворк предоставляет механизмы для собственного расширения/изменения. ExtJs не стал исключением.
По сути расширение это класс, наследованный от уже существующего в библиотеке и реализующий дополнительный функционал. Представим себе, что нам необходимо реализовать контрол, весьма похожий по своему назначению и функционалу на Ext.Panel, но с небольшим отличием - новая панель всегда должна быть квадратной формы (т.е. ширина всегда должна быть равна ее высоте).
1: SquarePanel = Ext.extend(Ext.Panel, { 2: //Устанавливаем размер панели по умолчанию
3: width: 100,
4: height: 100,
5: onResize: function(width, height) { 6: //Если размеры нашей панели были изменены, причем непропорционально - вернуть ей квадратную форму
7: if (width != height) { 8: this.body.setWidth(height);
9: }
10: //Вызываем базовый метод класса-родителя, т.е. - класса Panel.
11: SquarePanel.superclass.onResize.call(this, height, height);
12: },
13: //Добавляем метод который позволит нам изменять размеры нашей панели, указав лишь размер одной ее стороны
14: setSquareSize(length){ 15: this.body.setWidth(length);
16: this.body.setHeight(length);
17: }
18: });
Вот такая нехитрая конструкция Ext.extend позволяет создать новый класс, с дополнительными возможностями, а именно – новое поведение панели при изменении ее размеров и новый метод setSquareSize позволяющий установить новые размеры панели. Я акцентировал внимание на дополнительных возможностях, потому что все свойства, методы и события базового класса (Ext.Panel) теперь присущи и нашему классу. Если заглянуть внутрь реализации метода extend, то можно увидеть, что
1: //создаем пустую функцию-класс F
2: var F = function(){}, 3: //сюда будет записана ссылка на прототип дочернего класса (т.е. нашего собственного)
4: sbp,
5: //сюда будет записана ссылка на прототип родительского класса
6: spp = sp.prototype;
7: //прототипом класса F становится прототип родительского класса
8: F.prototype = spp;
9: //прототипом нашего же класса становится класс F
10: sbp = sb.prototype = new F();
11: //далее идет переопределение конструктора и назначение суперкласса
12: sbp.constructor=sb;
13: sb.superclass=spp;
14: //...много непонятных буков
Методы и свойства, которые мы переопределили (onResize, width и height) попадут в prototype нашего класса вот таким образом:
1: //sb - это наш класс SquarePanel, а overrrides - объект содержащий дополнения к нашему классу
2: Ext.override(sb, overrides);
О функции override, я расскажу в другой раз. Пока ограничусь лишь кратким описанием – функция Ext.override позволяет замещать функционал уже существующих классов, на ваши собственные реализации.
Далее мы можем расширять нашу панель точно также как и Ext.Panel
1: DifferentSquarePanel = Ext.extend(SquarePanel, {/* some additional logic here */})Что ж, довольно просто и элегантно. Впрочем, как и должно быть в любом уважающем себя фреймворке.
Сегодня я начинаю цикл статей посвященных фреймворку ExtJs. Это не будут туториалы или мануалы для пошагового изучения библиотеки. Я просто хочу поделиться своим опытом, поэтому начну с чего попало, а именно – с событий.
С изобретением GUI и Мыши, DOM-объекты, подобные кнопкам и текстовым полям стали обладать некими событиями. Например, click, mouseover, keydown и другими. По сути событие – это сообщение, вызов функции, генерируемое одной частью приложения (источником), которое извещает другую часть приложения (приемник или подписчик) о том, что что-то случилось.
1: <div id="theDiv" onclick="alert('You clicked me')">Click me!</div>
В ExtJs DOM-элементы оборачиваются в Ext.Element, поэтому подписка на тоже событие будет выглядеть так:
1: Ext.get('theDiv').on('click', function() {alert('You clicked me');});
При разработке сложных пользовательских интерфейсов, отдельные элементы DOMа получают особое назначение. К примеру, банальный div может быть заголовком колонки в таблице (я имею ввиду ExtJs Grid). Следовательно, когда пользователь захочет изменить порядок колонок в гриде и начнет перетягивать колонку за заголовок – в заголовке произойдут некие события (mouseover, mousedown, click, …). Каждый раз отслеживать порядок и детали “низкоуровневых” событий в своем коде рутинно. Поэтому ExtJs поддерживает собственную модель событий. К примеру, GridPanel поддерживает такие события как columnmove, cellclick, headerclick и другие.
Вся магия событий в ExtJs реализована в классе Ext.util.Observable. Чтобы наделить ваш компонент свойствами источника события, вам необходимо “унаследоваться” от Ext.util.Observable. Если же вы наследуете свой компонент от других компонентов, таких как Panel, Grid, Form, Tree и другие, то дополнительно наследоваться от Ext.util.Observable не обязательно. О принципе паттерна Observable и об особенностях его реализации на javascript можно прочесть на CodeProject.
После того как вы так или иначе унаследовались от Ext.util.Observable, вы можете добавить ваши собственные события примерно таким образом
1: this.addEvents( 'IAmHappy', 'IAmNotHappy' );
Где, this – это и есть ваш компонент.
Инициировать ваше событие также просто:
1: this.fireEvent('IAmHappy', this, firstArg, secondArg);
Когда мы вызываем функцию fireEvent, то Observable смотрит, подписывался ли на это событие (в данном случае – ‘IAmHappy’) кто-либо или нет. Если подписывался, то вызывает функцию-обработчик и устанавливает значение переменной this равное текущему объекту (этого мы добились указав this вторым параметром после названия события).
Также в эту функцию будут переданы параметры event и target. Где, event будет объект типа Ext.EventObject, а target – ссылка на HTMLElement. Следом за этими параметрами пойдут наши, собственные параметры firstArg и secondArg, коих может быть любое количество.
1: function IAmHappyListener(event, target, fristArg, secondArg){
2: ...
3: }
Если же Observable не нашел ни одного подписчика, то он, соответственно, ничего и не вызывает.
Простейший способ подписаться на события ExtJs я показал выше, но существуют также некоторые вариации:
Обработчик событий с задержкой (обработчик запуститься через 250 миллисекунд после события)
1: el.on('click', this.onClick, this, {delay: 250});
Обработчик событий с буферизацией (это означает, обработчик запуститься не чаще, чем раз в 100 миллисекунд и при условии, что событие произошло)
1: el.on('click', this.onClick, this, {buffer: 100});
Обработчик событий с условием распространения события
1: // prevent default
2: el.on('click', this.onClick, this, {preventDefault: true});
3:
4: // only stop propagation
5: el.on('click', this.onClick, this, {stopPropagation: true});
6:
7: // prevent default and stop propagation
8: el.on('click', this.onClick, this, {stopEvent: true});
preventDefault и stopPropagation означают тоже самое, что и соответствующие методы родных, джаваскриптовых событий.
Обработчик событий с другими опциями
1: el.on('click', this.onClick, this, {
2: single: true, // удаляет автоматически, после первого запуска
3: delegate: 'li.some-class' // автоматическая делегация событий
4: });
Добавление сразу нескольких обработчиков может выглядеть вот так:
1: el.on({
2: 'click' : {
3: fn: this.onClick,
4: scope: this,
5: delay: 100
6: },
7: 'mouseover' : {
8: fn: this.onMouseOver,
9: scope: this
10: },
11: 'mouseout' : {
12: fn: this.onMouseOut,
13: scope: this
14: }
15: });
Еще стоит упомянуть о такой полезной функции как relayEvents. Она позволяет делегировать события одного компонента – другому компоненту. К примеру, у вас есть панель A, содержащая другую панель B, а та в свою очередь содержит кнопку Save. По нажатию на кнопку Save произойдет ваше собственное событие onSomeSave. К сожалению, ни панель А, ни панель B, обработать это событие не может. Поэтому панели А необходимо генерировать это событие как свое собственное, в надежде, что кто-то сможет его обработать.

“Передать” события панели B в панель A и позволяет relayEvents:
1: panelA.relayEvents(panelB, ['onSomeSave']);
Фух, что-то длинный пост получился. Устал. А я ведь еще не рассказал об Ext.EventManager, о такой полезной функции как within и о многом другом. Ну чтож, это в другой раз.
Полезные материалы.
Полезные ссылки
Document Object Model Events
Introduction to Events
Tutorial:Events Explained
Tutorial:RelayEvents