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

в 12:36, , рубрики: javascript, jquery, jQuery UI, jquery widget

Все 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", {
    // These options will be used as defaults
    options: { 
      clear: null
    },
 
    // Set up the widget
    _create: function() {
    },

    // Use the _setOption method to respond to changes to options
    _setOption: function( key, value ) {
      switch( key ) {
        case "clear":
          // handle changes to clear option
          break;
      }
 
      // In jQuery UI 1.8, you have to manually invoke the _setOption method from the base widget
      $.Widget.prototype._setOption.apply( this, arguments );
      // In jQuery UI 1.9 and above, you use the _super method instead
      this._super( "_setOption", key, value );
    },
 
    // Use the destroy method to clean up any modifications your widget has made to the DOM
    destroy: function() {
      // In jQuery UI 1.8, you must invoke the destroy method from the base widget
      $.Widget.prototype.destroy.call( this );
      // In jQuery UI 1.9 and above, you would define _destroy instead of destroy and not call the base method
    }
  });
}( 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() {
    // Keep track of the generated list items
    this.items = this.items || $();

    // Use a class to avoid working on options that have already been created
    this.element.find( "option:not(.demo-multi-option)" ).each( $.proxy(function( i, el ) {
        // Add the class so this option will not be processed next time the list is refreshed
        var $el = $( el ).addClass( "demo-multi-option" ),
            text = $el.text(),

            // Create the list item
            item = $( "<li class='multi-option-item'>" + text + "</li>" )
                .data( "option.multi", el )
                .appendTo( this.list );

        // Save it into the item cache
        this.items = this.items.add( item );
    },this));

    // If the the option associated with this list item is no longer contained by the
    // real select element, remove it from the list and the cache
    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. Третий параметр, объект с данными, которые передаются в обработчики коллбека и события. Данные, передаваемые в этом объекте должны относиться только к текущему событию, и не должны отдаваться другими методами плагина.

Автор: leshaogonkov

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js