Pull to refresh

jQuery UI как инфраструктура для плагинов

Reading time 6 min
Views 21K

Введение


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 напильником, можно получить виджеты, которые сами читают свои настройки из разметки, сами находят витальные для себя элементы, и автоматически организовываются в иерархическую структуру. Но это явно тема для отдельной статьи.
Tags:
Hubs:
+85
Comments 51
Comments Comments 51

Articles