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

    Введение


    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 напильником, можно получить виджеты, которые сами читают свои настройки из разметки, сами находят витальные для себя элементы, и автоматически организовываются в иерархическую структуру. Но это явно тема для отдельной статьи.
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 51
    • 0
      Шикарная статья, спасибо, особенно понравилось «наследование» :)
      • –1
        Плохая статья, слишком маленькая. Пишите еще, и побольше.
        • 0
          Мне показалось, что вдаваться в дальнейшие подробности будет уже суесловием.
          • +1
            Я имел ввиду, что статей по Jquery UI побольше хочется видеть :-)
            • +2
              Собственно, про то, как это устроено, я рассказал:) Можно, конечно, написать какой-нибудь виджет и подробно его разжевать… Писать про «коробочные» — неинтересно, есть документация с демо и кодом.
              • +1
                По коробочным действительно все ясно, а вот написать свой и рассказать вообще про какие-нибудь интересные кейсы использования — я бы почитал) но это так… мнение одного человека.
                • +2
                  Про интересный кейс — будет, см. секцию «Заключение»;)
                  Написание своего тоже разжую, есть идея.
            • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            Имхо mootools лучше плюшку для этого реализовала, создание класса более наглядное, как и наследование, в jq с этим проблема. UI мне не особо нравится касательно кода. Обычно я юзаю jquery tools там реализация более грамотная и гибкая. Если нужен дополнительный функционал или сам пишу или ищу адекватный плагин.
            А переопределение функций — сомнительное решение
            • 0
              Переопределение?
              • +1
                Статья не про mootols. И даже не про создание классов.

                Существует много проектов, написанных на jQuery + UI, с этим приходится считаться и использовать то, что есть.
                • 0
                  ну да там пара функций стандартный jq переопределяются. Статья не о мутулс, я высказывал своё мнение, что если сравнивать создание классов и наследование то мутулс лучше. виджет — это класс, разве создавая виджет вы не класс создаете?
                  давно не использую ui как основу, разве что datepiker и так по мелочи если функционал 1 в 1. Проектов то много но инструменты то надо использовать с умом! Я же не критиковал вашу статью, что она не правильная, просто предложил на мой взгляд более удобные альтернативы.

                  Главный плюс что структура ui заставляет вас хоть как то использовать грамотно структурированный код. А не тупо куча кала в jq функционале )) к слову щас работаю с проектом, там вторую неделю хочу сломать нос создателю fancybox за криворукость, плагин из разряда я вообще прогить не умею поэтому подключу чтоб картинки показывал, вот верстальщик с ним наверстал кучу попапов, а теперь связать их — гемморой
                  • 0
                    Плагинов к jQuery много кривых, это да.
              • 0
                Статья хорошая, объясняет не совсем нормальную доку по написанию виджета на сайте jQueryUI.

                По поводу наследования, на сколько я помню, там оно очень бедненькое и методы не наследует в классическом смысле, а делает тупо $.extend, что не дает достучаться до методов предка. (Во всяком случае я способа не нашел)
                • 0
                  Ох, напутал я там в коде, прощу прощения. Поправил секцию про наследование.

                  Там да, $.extend, но не самого наследуемого виджета, а его prototype. Так что достучаться можно.
                • 0
                  Да, делал в одном из своих проектов свои виджеты. Весьма удобно. В частности я делал простую таблицу, а потом расширял ее до таблицы с сортировкой.
                  • 0
                    Помнится Илья Кантор проводил различие между плагинами и виджетами, я с ним в принципе согласен. Виджеты это блок, а плагин это функция. Соответственно плагины делаем на основе jQuery, виджеты на основе jQuery UI.
                    Это так, к слову пришлось=)
                    • 0
                      Я согласен. Но про плагины уж не стал писать, избитая тема, имхо:)
                      • 0
                        Это да, я про заголовок=)
                        • 0
                          Ах да, действительно.
                          Объясню почему. Во-первых, навешиваются они так же, как плагины. Во-вторых, граница размыта — «большой плагин или маленький виджет?». В-третьих, не хотелось начинать с грозного «виджеты» в заголовке — народ обычно думает об этих штуках, как о плагинах. В-четвертых, определение все-таки не общепринятое.
                          В следующей статье обязательно расскажу, в чем разница:)
                      • 0
                        это он о логическом скорее разделении. Все равно с програмной точки зрения плагин и виджет совершенно одно и тоже ибо к jq крепятся одинаково. просто существует плюшка $.widget котрая помогает структурировать код и делает некую часть работы за вас
                      • 0
                        Было бы не плохо, показать пример того, зачем все это нужно.

                        Например, написать свой простой полезный виджет на jQuery UI.
                        • 0
                          Затрудняюсь объяснить, зачем это нужно. Зачем вообще модульность в коде?:)

                          Напишу, есть планы.
                          • +1
                            Вот я не мега программист, мне пока не понятно. Без подколов, серьезно.

                            Нужны мне табы — использую $.tabs(), нужен всплывающий диалог — $.dialog(). Для jQuery написано огромное количество плагинов и я их просто использую.

                            Хотелось бы понять, нужно ли тратить время на изучение модульности, если и сейчас все окей?
                            Бывает же, что затраты времени и сил не окупаются.

                            Напишите пожалуйста подробнее, на примере. Типа, стандартным путем тут 10 строк кода, а если вот так — то 5 :-)
                            • +1
                              Прочтите «Совершенный код» Макконела. Вкратце, модульность дает несколько вещей.
                              Во-первых, возможность сосредоточиться на одной задаче за раз.
                              Во-вторых, можно распаллалелить задачи.
                              В-третьих, она упрощает поддержку — проще локализовать баг, проще расширять функциональность.

                              Вы используете виджеты из комплекта — прекрасно, в секции «Наследование» вы найдете то, что может пригодиться.
                              Я не призываю прямо щас сесть и все переписать на виджеты. Но лучше знать, что такая возможность есть, не правда ли?
                              • 0
                                Опять общие фразы.
                                Я же просил на примере показать. А в ответ — прочти книгу и все поймешь. Окей, наверное, мне пока рано в это лезть :-)
                                • 0
                                  Ну потому что это действительно общая вещь. Это просто хорошая практика. Это не артефакт, который дает +10 INT + 20 WIS:)
                                  • 0
                                    Поясню на примере.
                                    Почему Объектно ориентированное программирование — хорошо?
                                    Потому что, ты можешь написать одну функцию, которая будет выполнять какое-то действие, и вызывать ее в любом месте своей программы. К примеру, функция форматирования времени:

                                      function timeFormat($time) {
                                        return strftime('%d.%m.%Y', strtotime($time));
                                      }
                                    

                                    Теперь, когда нужно вывести в привычном виде дату, можно просто выполнить
                                    echo $this->timeFormat('2011-05-27');
                                    

                                    и получить время в нужном виде. Вместо указания точной даты, конечно же, надо подставлять переменные, из sql запроса, например.

                                    А еще, можно задать переменную для формата даты, и просто меняя ее получать другой результат.

                                      var $time_format = '%d.%m.%Y'; // Это шаблон для даты, можно поменять когда угодно
                                    
                                      function timeFormat($time) {
                                        return strftime($this->time_format, strtotime($time));
                                      }
                                    


                                    Вот в таком виде лично мне бы хотелось увидеть топик про модульность.
                                    • +1
                                      Вы немного перепутали ООП с функциональным программированием. Но автор, думаю, поймет аналогию)
                                      • 0
                                        ФП тут нет, вторая функция не является чистой:) ООП тут тоже не густо:)
                                        • 0
                                          Ну да, я видел "$this". Просто немного удивило это: «Почему Объектно ориентированное программирование — хорошо?
                                          Потому что, ты можешь написать одну функцию, которая будет выполнять какое-то действие, и вызывать ее в любом месте своей программы.» Но наверно просто не выспался и придираюсь)
                                      • 0
                                        Ну ей богу. ООП — это один частных случаев модульности. Функции — тоже.
                              • 0
                                Я согласен, что пример «на пальцах», например как во всеобщеизвестной доке в двух частях по написанию виджета (где также рассказывается почему нужно использовать виджеты), было бы ОЧЕНЬ хорошо иметь. Как раз для продолжения подоходит. ;)
                            • 0
                              спасибо большое, очень не хватает статей на русском по jQuery UI

                              с недавних пор успешно используем UI как стандарт кодирования в наших проектах, всё очень нравится, но вот о том, что на каждый элемент в коллекции (к которой применяется виджет) создается по отдельному экземпляру — не задумывался. теперь надо будет переписать старый код некоторый :)
                              • +1
                                В целом статья хорошая как вводная, но в ней есть ряд ошибок:

                                1) В разделе «Магия $.widget» про options:
                                Если при вызове виджета передать объект, то переданный объект будет «смерджен» (с помощью метода $.merge) с дефолтными настройками еще до вызова _create.

                                На самом деле используется $.extend(), ну и тогда уж объект будет «расширен».

                                2) Далее там же:
                                За работу с настройками отвечает метод setOption

                                Если это метод именно этого «нового» виджета — то вопросов нет, но если говорится о базовом методе (судя по примеру вызова) — то это не так. В виджетах есть public метод options( key, [ value ] ) и private методы _setOption, _setOptions.

                                И небольшое замечание по поводу раздела «Коллбэки»:
                                1) Если вызывается .bind() на элементе, то тип события надо указывать с префиксом в виде имени виджета, т.е. для примера в тексте должно быть: mw.bind('mywidgetonafterrender', function(evt, data) {console.log(data.theAnswer)});
                                2) В методе _trigger() тип события (строковый) всегда приводится к нижнему регистру, поэтому и в .bind() тип события должен быть в нижнем регистре полностью, но при этом для обработчика в опциях регистр учитывается — поэтому лучше для единообразия всё указывать в нижнем регистре.
                                • 0
                                  Ещё забыл про раздел «Наследование»:
                                  К сожалению, приходится явно указывать предка и вручную вызывать его конструктор.

                                  Ну без указания предка никуда, а вот если _create() пустой, то и объявлять его не надо и конструктор вручную вызывать тогда не придётся.
                                  • 0
                                    Ну, это-то понятно, что если конструктор тривиальный, то зачем его вызывать:)
                                    • 0
                                      Тут дело не в вызове, а в том, что если в вашем прототипе (описании виджета) не будет метода _create — то останется метод базового виджета. Т.о. если в вашем конструкторе нет ничего, кроме вызова конструктора базового виджета, то и писать его не надо, в таком случае останется конструктор базового виджета. Хотя и конструктором-то его можно назвать с большой натяжкой — это просто метод, который вызывается при создании инстанса виджета.
                                      • 0
                                        А, вот оно что… Спасибо, это я как-то не сообразил.
                                  • 0
                                    1) Да, конечно. У меня какое-то помрачение было:(

                                    2) Странно, у меня было другое ощущение… да и setOption вроде бы вполне работает.
                                    Я проверю и поправлю статью соответствующим образом.

                                    3) Вот это я не учел, спасибо.

                                    С вашего позволения, я внесу исправления в соответствующих местах, и упомяну вас в пост-скриптуме с ссылкой на этот комментарий. Не возражаете?
                                    • 0
                                      2) Да, ваш метод работает как метод, он же написан, почему бы ему не работать, я про то, что базового такого метода нет. Т.е. если убрать именно ваш метод, то работать будет нечему, а в пояснительном тексте говорится о базовом методе — возможно это просто неясно изложено было.

                                      Поправить, конечно, надо, на то и существует режим редактирования :) А уж упоминать ли мои комментарии в пост-скриптуме — это ваше право, комментарии всё равно никуда не денутся и редактировать их нельзя, но мне будет приятно.
                                      • 0
                                        Я имел ввиду, что если имплементировать метод setOption, то он будет отзываться на вызов .mywidget('option', …).
                                        • 0
                                          Сомнительный факт для меня. Вот если метод "_setOption" — то соглашусь, что он будет вызываться, но при этом надо учитывать, что метод базового виджета тоже не просто так и его либо надо вызывать, либо умышленно и осознанно не вызывать, отвечая за последствия.
                                          • 0
                                            Расскажите поподробнее, пожалуйста, какой механизм работает, когда мы делаем .mywidget('option', …)?
                                            • 0
                                              Вызывается метод виджета .option(), который в свою очередь если требуется чтение — то возвращает значение нужной опции (или всех), а если требуется запись — то вызывает метод _setOptions(), а тот в свою очередь вызывает уже _setOption().
                                              Детали можно посмотреть самому — там всё просто: github.com/jquery/jquery-ui/blob/1-8-stable/ui/jquery.ui.widget.js#L186
                                              • 0
                                                Все, я понял вас, спасибо.

                                                А как бы вы доопределили поведение виджета в ответ на изменение того или иного ключа, не трогая унаследованные функции?
                                                • 0
                                                  Если речь про опции, то
                                                  1) не трогая можно сделать метод(ы), который будет что-то делать и менять опцию, а в описании опции тогда надо писать, что напрямую её менять не рекомендуется
                                                  2) либо (правильнее на мой взгляд) переопределить метод _setOption() и внутри нового метода обработать свой специфичный ключ опции и/или вызвать метод базового виджета.
                                                  • 0
                                                    Ок, так и запишем:)
                                  • 0
                                    И отдельно хочу заметить про заключение:
                                    Допилив метод $.widget напильником, можно получить виджеты, которые сами читают свои настройки из разметки, сами находят витальные для себя элементы, и автоматически организовываются в иерархическую структуру. Но это явно тема для отдельной статьи.

                                    Виджеты и так умеют читать свои настройки из разметки при наличии и соответствующей настройке плагина $.metadata.
                                    Если под «допилить напильником» имеется в виду «взять исходник и поправить его» — то, на мой взгляд, это плохая идея. Потому что того, что есть сейчас вполне хватает для тех задач, которые встают перед виджетами jQuery UI. А вот если автор хочет написать свою фабрику виджетов (вполне можно взять за отправную точку идеи и часть кода из существующей фабрики) — то это благородная цель, но важно помнить, что у этой фабрики будут свои цели, задачи и область применения, и они должны быть обоснованными. Т.е. я хочу сказать, что метод, который используется в 1-ом виджете из 10, не нужен в базовом классе, он просто будет загромождать код.
                                    • 0
                                      Ну, то, что я сделал в своем pet-project, на данный момент больше похоже на monkey-patсhing, это да. Но просто потому что поправить нужно было буквально только в двух местах; переписывать из-за этого весь код заново не хотелось. Связано это с тем, как работает фабрика виджетов с опциями и всем остальным.

                                      Кроме этого, мне хотелось сделать что-то все-таки цельное — уйти от концепции библиотеки методов, на каковую так ориентирована jQuery, и приблизиться к неким зачаткам фреймворка для построения приложений. При этом мне хочется описывать мои виджеты декларативно — отсюда желание автоматизации чтения настроек и т.д.
                                    • 0
                                      А почему статья не в блог jQuery?

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