Как стать автором
Обновить

Фабрика виджетов jQuery UI

Время на прочтение9 мин
Количество просмотров56K
Автор оригинала: Scott González
Все jQuery UI виджеты создаются на простой основе — Фабрике Виджетов. Она обеспечивает гибкую основу для создания сложных, структурированных плагинов с совместимым API. С её помощью можно создавать не только плагины jQuery UI, но и любые объектно-ориентированные компоненты, не изобретая велосипедов. Она не зависит от других компонентов jQuery UI, наоборот, большая часть компонентов UI зависит от неё.

Что это?


Фабрика виджетов это метод глобального объекта jQuery — jQuery.widget — принимающий 2 или 3 аргумента.

jQuery.widget(
    'namespace.widgetname',

    namespace.superwidget, // не обязательно - прототип существующего виджета
                           // от которого будет наследоваться ваш виджет

    {...}                  // литерал объекта,
                           // который послужит прототипом вашего виджета
);

Первый аргумент строкового типа, содержащий пространство имен для вашего виджета, и его имя, разделяются точкой. Пространство имен обязательно, и ссылается на место в глобальном объекте jQuery, где будет храниться прототип виджета. Если пространство имен не задано, фабрика создаст вам его. (прим. переводчика — Но плагин без неймспейса попросту не заработает, во всяком случае у меня не получилось. Да и вообще неймспейсы полезно.) Имя плагина хранится как имя плагина и прототипа. Например:

jQuery.widget("demo.multi", {...})

создаст jQuery.demo.multi и jQuery.demo.multi.prototype.

Второй (не обязательный) аргумент, прототип для наследования от него. Например, в jQuery UI есть плагин «mouse», на котором основаны все плагины для взаимодействий [с мышью]. Для достижения этого, draggable, droppable и т. п. наследуются от плагина «mouse»:

jQuery.widget( "ui.draggable", $.ui.mouse, {...} );

Если вы не указываете этот аргумент, виджет наследуется напрямую от «основного виджета» jQuery.Widget (обратите вниманию на разницу между jQuery.widget с маленькой w и jQuery.Widget с большой W).

Последний аргумент — литерал объекта, который будет использоваться в качестве прототипа для каждого экземпляра виджета. Фабрика создает цепочку прототипов, соединяя прототип виджета со всеми виджетами от которого он наследуется, вплоть до базового jQuery.Widget.

При вызове jQuery.widget в прототипе jQuery (jQuery.fn) создается новый метод, соответствующий имени виджета, в нашем случае это будет jQuery.fn.multi. Метод .fn служит интерфейсом между элементами DOM, получаемыми в объекте jQuery и экземплярами виджета. Экземпляр виджета создается для каждого элемента в объекте jQuery.

Польза


Упрощенный подход, описанный в Гайдлайнах по разработке плагина, оставляет много вопросов с конкретной реализацией, когда дело касается структурированного кода и ООП ориентированных плагинах. Кроме того, они не предлагают никаких решений для общих задач. Фабрика виджетов предоставляет API jQuery UI, для связи с экземпляром плагина и решения нескольких повторяющихся задач.

  • Создание неймспейса и прототипа
    Кроме этого, для запросов и фильтрации генерируется псевдо-селектор на основе неймспейса и имени, например $(":demo-multi").
  • Связь между прототипом и jQuery.fn
    Реализуется через jQuery.widget.bridge
  • Слияние пользовательских настроек с настройками по умолчанию
    Настройки по умолчанию могут быть изменены (прим. переводчика — видимо имеются ввиду настройки по умолчанию для всех экземпляров виджета — можно изменить дефолтные настройки в прототипе своего виджета в конфиге для конкретной страницы, например).
  • Экземпляр плагина доступен через $("#something").data("pluginname")
    Ссылка на объект jQuery, содержащий DOM элемент (прим. переводчика — имеется ввиду ссылка на конкретный DOM элемент, на котором в данный момент отрабатывает экземпляр плагина) доступна в качестве свойства экземпляра this.element, поэтому становится очень легко взаимодействовать с плагином и элементом.
  • Методы виджета доступны через аргументы метода прототипа jQuery — $("#something").multi("refresh") — или напрямую через экземпляр — $("#something").data("multi").refresh().
  • Предотвращает появление нескольких экземпляров на одном элементе
    Механизм коллбеков, на которые может подписываться пользователь: this._trigger("clear")
    • Подписаться:
      $( "#something" ).multi({ clear: function( event ) {} });

    • Или, если через .bind:
      $( "#something" ).bind( "multiclear", function( event ) {} );

      (прим. переводчика — обратите внимание имя события склеилось из имени плагина и события)

  • Механизм облегчающий изменение опций плагина после инициализации
  • Просто включать/отключать или уничтожать экземпляр и возвращаться к изначальному состоянию. (прим. переводчика — смотрите дальше приватные методы _destroy и т. д.)


Создание собственного прототипа


Инфраструктура

Литерал объекта, передаваемый в качестве прототипа может быть устроен как угодно, но он обязательно должен содержать options (прим. переводчика — опции по умолчанию), коллбеки _create, _setOption, и destroy.

Пример
(function( $ ) {
  $.widget( "demo.multi", {
    // Эти опции будут использоваться в качестве опций по умолчанию
    options: { 
      clear: null
    },
 
    // Создаем виджет
    _create: function() {
    },

    // Используем метод _setOption для реагирования на изменения в настройках
    _setOption: function( key, value ) {
      switch( key ) {
        case "clear":
          // обрабатываем изменения
          break;
      }
 
      // В jQuery UI 1.8, мы вручную вызываем метод _setOption в родительском виджете
      $.Widget.prototype._setOption.apply( this, arguments );
      // В jQuery UI 1.9 и выше, мы будем использовать метод _super
      this._super( "_setOption", key, value );
    },
 
    // Используем метод destroy для очистки любых изменений в DOM, которые сделал наш виджет
    destroy: function() {
      // В jQuery UI 1.8 нужно вызывать метод destroy из родительского виджета
      $.Widget.prototype.destroy.call( this );
      // В jQuery UI 1.9 и выше вместо этого мы определим метод _destroy и не будем вызывать метод базового виджета
    }
  });
}( jQuery ) );

Инкапсуляция в методах

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

Например, в гипотетическом виджете, расширяющим <select multiple>, кто-то захочет пробежаться по дочерним <option>, для создания соответствующих <li> и <ul>. Это можно реализовать через метод _create:

_create: function() {
    var self = this;
    this.list = $( "<ul>" ).insertAfter( this.element );

    this.element.hide().find( "option" ).each(function( i, el ) {
        var $el = $( el ),
            text = $( el ).text(),
            item = $( "<li class='multi-option-item'>" + text + "</li>" );

        item.appendTo( self.list ).click(function(){ 
            console.log( $el.val() );
        });
    });
}


К сожалению, если оставить так код в _create, это создаст сложности в определении связей между оригинальным элементом <option> и пунктами списка который мы создаем, или проблему при изменении состояния элементов <option>, которые были добавлены или удалены из изначального <select> уже после инициализации виджета. Вместо этого мы сделаем метод refresh, отвечающий за работу с элементом, и вызывающийся из метода _create. Мы также вынесем отдельно обработку щелчков по элементам списка, и будем использовать делегирование события, что бы не навешивать новые обработчики, после создания нового пункта.

Пример
_create: function() {
    this.list = $( "<ul>" )
        .insertAfter( this.element )
        .delegate( "li.multi-option-item", "click", $.proxy( this._itemClick, this ) );
    this.element.hide();
    this.refresh();
},

refresh: function() {
    // Отслеживаем сгенерированные пункты списка
    this.items = this.items || $();

    // Используем класс, что бы не затронуть уже созданные пункты
    this.element.find( "option:not(.demo-multi-option)" ).each( $.proxy(function( i, el ) {
        // Добавляем класс, что бы опция не обрабатывалась при следующем обновлении
        var $el = $( el ).addClass( "demo-multi-option" ),
            text = $el.text(),

            // Создаем пункт списка
            item = $( "<li class='multi-option-item'>" + text + "</li>" )
                .data( "option.multi", el )
                .appendTo( this.list );

        // Сохраняем в кеш
        this.items = this.items.add( item );
    },this));

    // Если опции уже нет в селекте, удаляем созданную для неё замену из списка и кеша
    this.items = this.items.filter( $.proxy(function( i, item ) {
        var isInOriginal = $.contains( this.element[0], $.data( item, "option.multi" ) );
        if ( !isInOriginal ) {
            $( item ).remove();
        }
        return isInOriginal;
    }, this ));
},
_itemClick: function( event ) {
     console.log( $( event.target ).val() );
}

Приватные v. Публичные методы

Как вы наверное заметили, некоторые методы мы написали с нижним подчеркиванием вначале, в то время как некоторые без него. Методы с префиксом обрабатываются как «приватные». Фабрика блокирует все попытки вызвать их через $.fn

$( "#something" ).multi( "_create" )

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

$( "#something" ).data( "multi" )._create()

Как же принять правильное решение? Если пользователям ваших виджетов понадобятся определенные методы, то сделайте их публичными. Пример с refresh показателен: раз пользователь может управлять элементами селекта, мы должны обеспечить ему возможность обновить его. С другой стороны, служебная функция для обработки событий, как например _itemClick, требуется только для внутреннего использования, и совсем не нужна в публичном интерфейсе плагина.

Свойства

this.element

Элемент, используемый экземпляром плагина. Например:

$( "#foo" ).myWidget()

В этом случае this.element будет объектом jQuery, содержащим элемент с id foo. Для нескольких элементов, для которых вызывается .myWidget(), отдельный экземпляр плагина будет вызван для каждого элемента. Другими словами this.element будет всегда содержать только один элемент.

this.options

Опции, используемые для настройки плагина. При создании экземпляра, любые опции, переданные пользователем, автоматически объединяются с заданными в $.demo.multi.prototype.options. Пользовательские опции перезаписывают опции по умолчанию.

this.namespace

Пространство имен плагина, в нашем случае «demo». Как правило не используется в плагинах.

this.name

Имя плагина, в нашем случае «multi». Немного полезней чем this.namespace, но в большинстве случаев также не используется.

this.widgetEventPrefix

Свойство, использующееся для именования событий плагина. Например, dialog имеет коллбек close, при его исполнении всплывет событие dialogclose. Имя события состоит из префикса события и имени коллбека. По умолчанию значение widgetEventPrefix такое же как и имя виджета, но может быть перезаписано. Например, если пользователь начал перетаскивать элемент, мы не хотим что бы всплывало событие draggablestart, мы хотим что бы оно называлось dragstart, для этого мы делаем префикс события равным «drag». Если имя коллбека совпадает с префиксом события, то событие будет без префикса. Это позволит избежать событий наподобие dragdrag.

this.widgetBaseClass

Полезно для создания имен классов элементов виджета. Например, если нужно пометить элемент как активный

element.addClass( this.widgetBaseClass + "-active" )

Для большинства плагинов это не обязательно, потому что проще написать что-то вроде .addClass( "demo-multi-active" ). Приведенный пример актуальнее для самой фабрики и абстрактных плагинов, таких как $.ui.mouse.

Методы

(прим. переводчика - лучше всего, конечно, ознакомиться напрямую с документацией)

_create

Метод, в котором настраивается всё, относящееся к вашему виджету — создание элементов, навешивание событий и т. д. Метод вызывается один раз, сразу после создания экземпляра.

_init

Метод, вызывающийся каждый раз при вызове виджета, вне зависимости от количества передаваемых аргументов. Во время первого вызова _init вызывается после _create. Он также может быть вызван в любое время после создания виджета, в этом случае _init может служить для ре-инициализации, без выполнения уничтожения и повторного создания.

destroy

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

option

Используется для задания опций после создания экземпляра. Сигнатура метода похожа на методы .css() и .attr(). Вы можете задать только имя, что бы получить значение, или имя вместе со значением, для того что установить его, либо передать объект, для установки нескольких значений. Метод вызывает _setOptions, поэтому он не должен меняться сторонними плагинами.

_setOptions

Приватный метод, используется для установки настроек после создания экземпляра. Метод вызывает _setOption, поэтому он не должен меняться сторонними плагинами.

_setOption

Вызывается, когда пользователь меняет значение через метод option. Этот метод можно менять в своем плагине, если нужно особое поведение при изменении определенных опций. Например, если в диалоговом окне меняется значение заголовка окна, нужно запустить обновление заголовка.

_setOption: function(key, value) {
    if (key === 'title') {
        this.titleElement.text(value);
    }
    $.Widget.prototype._setOption.apply(this, arguments);
}

Вызывая родительский метод _setOption, мы устанавливаем новое значение опции. Это не должно выполняться _setOption. Иногда требуется сравнить старое и новое значения, что бы определить корректное поведение. Вы можете сравнить this.options[key] со значением, так как вызов родительского метода _setOption происходит уже в самом конце. Если вам не нужно ничего сравнивать, вы можете делать вызов родительского _setOption в самом начале метода.

enable

Хелпер, вызывающий option('disabled', false). Вы также можете отлавливать вызов этого хелпера, проверяя:

if (key === "disabled")

в вашем _setOption.

disable

Хелпер, вызывающий option('disabled', true). Вы также можете отлавливать вызов этого хелпера, проверяя:

if (key === "disabled")

в вашем _setOption.

_trigger

Этот метод необходимо использовать для всех коллбеков. Имя выполняемого коллбека единственный обязательный параметр. Все коллбеки также триггерят событие (см. описание this.widgetEventPrefix выше). Вы можете также передать объект события, запустившего коллбек. Например, событие drag запускается событием mousemove, и оно должно передаваться в _trigger. Третий параметр, объект с данными, которые передаются в обработчики коллбека и события. Данные, передаваемые в этом объекте должны относиться только к текущему событию, и не должны отдаваться другими методами плагина.
Теги:
Хабы:
+34
Комментарии16

Публикации

Изменить настройки темы

Истории

Работа

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн