Google Closure для начинающих. Компоненты

Продолжим изучение 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.

Читаем еще

Offtopic

Я тут еще в твиттер на досуге пишу, так что следите и не пропустите ;)

3 thoughts on “Google Closure для начинающих. Компоненты”

  1. Доброго дня

    Не подскажете почему в вашем примере можно убрать

    goog.require('goog.ui.Component');

    из кода файла js и просто вставить его в html между тегом script? (или почему этот код в 2-х местах и в js и в html)

    Причем если оставить этот код в начала js файла, но убрать из тега script, то ничего не работает? (говорить что поле Component не определено).

  2. Спасибо. Теперь я разобрался в создании компонентов. Как то программирование интереснейшие стало.

  3. exitDocument не обязательно дергать, хватает dispose который сам дергает exitDocument(). Также можно не морочится с Unlisten если создавать listen через this.getHandler().listen(); т.к. в exitDocument суперкласса вызывается this.getHandler().removeAll() что автоматически удалит все Listen.
    По опыту работы, самое важное – это добавлять правильно новый component к parent и в disposeInternal удалять все this переменные.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.