Введение
jQuery UI больше всего известен как набор готовых виджетов. Главное их преимущество, на мой взгляд, — консистентное API: каждый виджет управляется одинаково. Второе их преимущество — они хранят свое состояние: если повторно навесить виджет на элемент, то результатом будет уже существующий инстанс виджета.
Но jQuery UI — это не только набор окошечек и табов (далеко не всеми любимых). Это еще целая инфраструктура для создания своих виджетов: с удобным консистентным API, с хранением состояния и с возможностью наследования. Как ни странно, это для многих новость, в результате чего и появилась эта статья — так же, как это было новостью для меня всего несколько месяцев назад.
Лирическое отступление
Дабы не тратить время и место, везде ниже по коду подразумевается, что window.$ == window.jQuery, undefined никто не испортил, и что мы подключаем jQuery, только jQuery и ничего, кроме jQuery, и что все объявления обернуты в нечто вроде этого:
(function($) {
// наш код
})(jQuery)
* This source code was highlighted with Source Code Highlighter.
Так же подразумевается, что читатель неплохо знаком с jQuery и хотя бы читал документацию от jQuery UI.
Магия $.widget
Вся магия заключается в методе
$.widget
. Он принимает 2 (или 3 — в случае наследования) параметра. Официально этот метод называется «фабрика виджетов».Первый параметр — строка, она содержит неймспейс и собственно имя виджета, разделенные точкой. Например,
"my.myWidget"
. Неймспейс обязателен; вложенность не поддерживается. Второй параметр — литерал объекта, который, собственно, и описывает наш виджет:$.widget("my.myWidget", {
options: {
greetings: "Hello"
},
_create: function() {
this.element.html(this.options.greetings);
}
})
* This source code was highlighted with Source Code Highlighter.
Функция, лежащая в поле под именем
_create
, служит конструктором, и будет вызвана при создании инстанса виджета; на этот инстанс и указывает this
.this.element
— это элемент, на который был навешен виджет. Это всегда одиночный элемент, а не коллекция (как в случае обычных плагинов); если навешивать виджет на jQuery-объект, который содержит больше одного элемента, то будет создано столько инстансов, сколько элементов.В поле
options
хранятся дефолтные настройки виджета. Это поле наследуется, так что оно всегда будет в виджете, даже если не объявлять его явно.Если при вызове виджета передать объект, то переданный объект будет «смерджен» (с помощью метода
$.merge
) с дефолтными настройками еще до вызова _create
.За работу с настройками отвечает метод
setOption
: $.widget("my.myWidget", {
options: {
greetings: "Hello"
},
_create: function() {
this._render();
},
_render: function() {
this.element.html(this.options.greetings);
},
setOption: function(key, value) {
if (value != undefined) {
this.options[key] = value;
this._render();
return this;
}
else {
return this.options[key];
}
}
})
* This source code was highlighted with Source Code Highlighter.
Используется это так же, как в любом стандартном виджете:
var mw = $('.mywidget').myWidget({greeting: 'Hi there!'})
console.log(mw.myWidget('option', 'greeting')); // 'Hi there!'
mw.myWidget('option', 'greeting', 'O HAI CAN I HAZ CHEEZBURGER?');
* This source code was highlighted with Source Code Highlighter.
Приватные и публичные методы
К методу виджета можно обратиться примерно так же, как мы обращаемся к настройкам:
$.widget("my.myWidget", {
options: {
greetings: "Hello"
},
_create: function() {
this._render();
},
_render: function() {
this.element.html(this.options.greetings);
},
sayHello: function(saying) {
alert(saying);
},
_setOption: function(key, value) {
if (arguments.length == 1) {
this.options[key] = value;
this._render();
return this;
}
else {
return this.options[key];
}
}
})
// …
mw.myWidget("sayHello", 42);
* This source code was highlighted with Source Code Highlighter.
Но для этого этот метод должен быть публичным. Как сделать метод публичным в парадигме UI-ных плагинов? Это просто: публичными методами движок виджетов считает те, имена которых не начинаются с подчеркивания. Все остальные методы — приватные. Поля виджетов, не являющиеся функциями, всегда только приватные.
Это, конечно, не в полном смысле public и private методы, а их эмуляция, впрочем, достаточная для того, чтобы разграничить доступ.
Коллбэки
По сути, это просто шорткаты для привязки к пользовательским событиям внутри виджета. В виджет они передаются так же, как и настройки.
$.widget("my.myWidget", {
options: {
greetings: "Hello"
},
_create: function() {
this._render();
},
_render: function() {
this.element.html(this.options.greetings);
this._trigger("onAfterRender", null, {theAnswer: 42})
}
})
// …
var mw = $(".mywidget").myWidget(
{
greeting: "Hi there!",
onAfterRender: function(evt, data) {
console.log(data.theAnswer)
}
})
* This source code was highlighted with Source Code Highlighter.
Это эквивалентно старому доброму
.bind
в таком виде:mw.bind('onAfterRender.myWidget', function(evt, data) {console.log(data.theAnswer)})
* This source code was highlighted with Source Code Highlighter.
Деструкторы
Идущие «из коробки» виджеты имеют обыкновение генерировать кучу разметки. Хорошо это или плохо — вопрос дискусионный. Но, отчасти поэтому, отчасти потому, что ссылка на инстанс виджета записывается в expando-атрибут DOM-элемента — надо вызывать деструктор, когда вы уничтожаете виджет.
В качестве деструктора вызывается метод под названием destroy. К сожалению, его всегда надо вызывать явно. Чтобы было полное счастье, внутри деструктора должен быть следующий вызов:
$.Widget.prototype.destroy.call(this);
* This source code was highlighted with Source Code Highlighter.
Наследование
Одна из самых вкусных вещей, хотя по ней практически нет информации.
Если вторым аргументом передать какой-то другой виджет A (наш виджет в этом случае идет третьим аргументом), новый виджет B будет его потомком.
Предположим, в нашем приложении — куча диалоговых окон, и все — модальные. При этом они не должны закрываться по Esc. Писать каждый раз вот такое совсем не хочется:
$('.dialog').dialog({
modal: true,
closeOnEscape: false,
// … еще куча настроек, которые тоже могут быть
// одинаковы для всех диалогов…
})
* This source code was highlighted with Source Code Highlighter.
Мы можем отнаследоваться от стандартного диалога и переопределить дефолтные настройки:
$.widget("my.mydlg", $.ui.dialog, {
options: {
modal: true,
closeOnEscape: false,
},
_create: function() {
$.ui.dialog.prototype._create.call(this);
}
})
* This source code was highlighted with Source Code Highlighter.
Теперь заменяем во всем коде вызовы .dialog на .mydlg и наслаждаемся уменьшением дублирования. К сожалению, приходится явно указывать предка и вручную вызывать его конструктор.
Заключение
Мне кажется, что UI-ные виджеты — это неплохое средство модуляризации кода. В небольших и средних проектах они сами себе могут обеспечить достаточную инфраструктуру приложения.
При этом не надо тащить весь, довольно увесистый, jQueryUI — достаточно компонента core.
Паттерн, который лежит в основе этого виджетного движка, создатели называют bridge (хотя, конечно, метод
$.widget
— это фабрика). Допилив метод $.widget напильником, можно получить виджеты, которые сами читают свои настройки из разметки, сами находят витальные для себя элементы, и автоматически организовываются в иерархическую структуру. Но это явно тема для отдельной статьи.