
Продолжим изучение Google Closure Library. В данной статье я постараюсь отклонится от «jQuery way» и продемонстрировать как удобны в использовании и просты в создании компоненты фреймворка.
Давненько я технических статей не писал, отвыкли небось, но отвлекать не буду, читайте на здоровье ;)
Это все же, в большей степени, перевод статьи из официальной документации, но он слегка сокращен и адаптирован
Много вкусного несет в себе пакет goog.ui, я совсем чуть-чуть рассказывал о нем в предыдущей статье, но напомню — этот пакет содержит в себе готовые компоненты для реализации интерфейсов: кнопки, меню, календари, и т.д.
Основой всех компонентов является класс goog.ui.Component, и для создания своих компонентов без него не обойтись. Он требует от нас определенную структуры класса состоящую из шести методов, которые описывают жизненный цикл компонента (в хронологическом порядке):
| Метод | Описание метода (жизненного цикла) |
|---|---|
| constructor | создание компонента |
| createDom() | создание DOM структуры компонента |
| decorateInternal() (optional) | |
| enterDocument() | после создания DOM структуры будет запущен данный метод (обработчики событий вешаем тут) |
| exitDocument() | перед удалением DOM структуры (чистим обработчики событий) |
| dispose() | удаление компонента |
Отдельно особняком стоит метод canDecorate(existingElement) — он расскажет нам можно ли создать компонент основываясь на уже существующих элементах (existingElement). По умолчанию, суперкласс всегда возвращает true.
И так, перейдем к созданию своего компонента…
Инициализация
Начинается все с шага ноль — подключения goog.ui.Component, и объявления имени нашего компонента.
goog.provide('tutorial.component');
goog.require('goog.ui.Component');
Теперь шаг первый — это создание конструктора компонента:
tutorial.component = function(opt_label, opt_domHelper) {
// вызов конструктора предка
goog.ui.Component.call(this, opt_domHelper);
/**
* Заголовок виджета
* @type String
* @private
*/
this.initialLabel_ = opt_label || 'Link';
/**
* Наш обработчик событий
* @type goog.events.EventHandler
* @private
*/
this.eh_ = new goog.events.EventHandler(this);
/**
* Обработчик клавиатуры, у нас в компоненте не используется, но правила именования следует соблюдать
*
* @type {goog.events.KeyHandler?}
* @private
*/
this.kh_ = null;
};
// наследуем goog.ui.Component
goog.inherits(tutorial.component, goog.ui.Component);
Создание DOM структуры
После создания экземпляра компонента необходимо создать соответствующую DOM структуру, для этого потребуется реализовать метод createDom() и decorateInternal() (опционально):
| createDom() | создание DOM структуры компонента, и сохранение в this.element_ |
| decorateInternal() (optional) | создаем компонент используя уже существующую DOM структуру, и так же сохраняем в this.element_ |
Ну и пример кода:
/**
* создаём необходимую нам DOM структуру
* в нашем примере необходим лишь один div
*/
tutorial.component.prototype.createDom = function() {
// вызываем decorateInternal который внесет изменения в новосозданный элемент
this.decorateInternal(this.dom_.createElement('div'));
};
/**
* изменяем существующую структуру под наши нужды
*
* @param {HTMLElement} element DIV который мы изменяем
*/
tutorial.component.prototype.decorateInternal = function(element) {
tutorial.component.superClass_.decorateInternal.call(this, element);
var elem = this.getElement();
goog.dom.classes.add(elem, 'tutorial-component');
goog.dom.setTextContent(elem, this.initialLabel_);
};
Еще один шаг — после того как построили DOM
Зачастую, инициализация компонента требует уже заранее подготовленную структуру DOM. К примеру, вы создаете popup меню и вам необходимо повесить обработчик на div с id=”menu”, но этот элемент мы создаем лишь на этапе createDom, таким образом у нас получается неувязочка, но не стоит беспокоится, создатели фреймворка приберегли на этот случай метод enterDocument():
| enterDocument() | в данном методе реализуют функционал, который требует наличие структуры компонента в DOM |
Реализуя метод enterDocument() необходимо всегда вызывать одноименный метод суперкласса, который вызовет все дочерние компоненты. Собственно, в методе enterDocument() лучше всего описывать методы, которые вешают обработчики событий на элементы DOM, и это тем более обязательно для элементов, которые не являются частью компонента (а лишь добавляют нам пачку зависимостей =).
Но вернемся к нашему примеру:
tutorial.component.prototype.enterDocument = function() {
// вызываем метод супер класса
tutorial.component.superClass_.enterDocument.call(this);
// простой обработчик на click
this.eh_.listen(
this.getElement(),
goog.events.EventType.CLICK,
this.onDivClicked_
);
};
Резюмируя — не хочешь проблем с обработчиками — вешай их в enterDocument().
Методы render() и decorate()
Если мы все делали правильно, то можем попробовать инициализировать свой компонент используя метод render():
var comp1 = new tutorial.component("Button");
comp1.render(goog.dom.$('button1'));
Метод render() вызывает createDom(), получившийся element_ помещает в DOM и затем вызывает enterDocument(). Если метод render() дергать с параметром Element (в примере выше это “button1”), то element_ будет помещен внутрь него, иначе станет потомком body.
В примере есть и второй способ вызова компонента:
var comp2 = new tutorial.component("Button Two");
comp2.decorate(goog.dom.$('button2'));
Метод decorate() получает уже готовый element_ в качестве параметра, который и скармливает методу decorateInternal(), и затем точно так же вызывается enterDocument().
Внутри компонента вы не должны вызывать методы
render()илиdecorate(). Хотя, если вы не используете методыrender()илиdecorate(), следует вызыватьenterDocument()после того как будут добавлены все необходимые элементы компонента в DOM
«Я тебя породил, я тебя и уничтожу»
Создавая компонент не забИвайте описывать методы для подчистки DOM’а после его удаления, для этого следует подчистить все, что мы наворотили в методах createDom() и enterDocument(). В этом нам помогут следующие методы:
| exitDocument() | вызывается перед удалением DOM структуры — тут чистим обработчики событий |
| dispose() | удаление компонента и DOM элементов |
Ну и пример метода exitDocument():
tutorial.component.prototype.exitDocument = function() {
tutorial.component.superClass_.exitDocument.call(this);
this.eh_.unlisten(
this.getElement(),
goog.events.EventType.CLICK,
this.onDivClicked_
);
this.eh_.unlisten(
this.getElement(),
[goog.events.EventType.MOUSEOVER, goog.events.EventType.MOUSEOUT],
this.onDivHover_
);
};
Обратите внимание на вызов метода
exitDocument()нашего суперкласса — данная махинация необходима для вызова методовexitDocument()у всех дочерних компонентов (если таковые имеются). Этот вызов обязателен!
Теперь dispose():
tutorial.component.prototype.dispose = function() {
if (!this.isDisposed()) {
tutorial.component.superClass_.dispose.call(this);
this.eh_.dispose();
if (this.kh_) {
this.kh_.dispose();
}
}
};
Опять же — дергаем супер класс, затем подчищаем обработчики событий.
В заключение
Как видите — в создании компонента нет ничего сложного, стоит лишь следовать простым правилам, которые диктует на фреймворк, и вы не запутаетесь и все будет работать.
И да, пощупать пример можно на страничке closure-tutorials/component.html.
Читаем еще
- Introduces goog.ui.Component — первоисточник статьи
- Google Closure для начинающих
Offtopic
Я тут еще в твиттер на досуге пишу, так что следите и не пропустите ;)
Доброго дня
Не подскажете почему в вашем примере можно убрать
goog.require('goog.ui.Component');из кода файла js и просто вставить его в html между тегом script? (или почему этот код в 2-х местах и в js и в html)
Причем если оставить этот код в начала js файла, но убрать из тега script, то ничего не работает? (говорить что поле Component не определено).
Спасибо. Теперь я разобрался в создании компонентов. Как то программирование интереснейшие стало.
exitDocument не обязательно дергать, хватает dispose который сам дергает exitDocument(). Также можно не морочится с Unlisten если создавать listen через this.getHandler().listen(); т.к. в exitDocument суперкласса вызывается this.getHandler().removeAll() что автоматически удалит все Listen.
По опыту работы, самое важное – это добавлять правильно новый component к parent и в disposeInternal удалять все this переменные.