CornerJS, или директивы «как в AngularJS», только лучше

image

Постоянно создавая сложные веб-проекты с нуля, начинаешь замечать, что примерно треть — а в некоторых случаях половина и больше — кода в действительности автономна, и привязана только к определенному DOM-элементу.

В рабочих проектах это может сводиться к чему-то вроде

function pageChange(){
    if ($(‘.element-carousel').length>0) {$('.element-carousel').initCarousel()}
    if ($('.element-scrollbox').length>0) {$('.element-scrollbox').initScrollbox()}
…


А может и не сводиться, и в каждом условном контроллере (колбэке на смену определенной страницы) мы вызываем код, связанный с определенными элементами.

Знакомо? Думаю, да. Считаете ли вы этот подход неправильным? Если первый ответ – да, то уверен, что и второй тоже да.

Хотите узнать, как можно сделать правильно, аккуратно и красиво?

Директивы

В AngularJS есть, как они пишут, уникальная фича – директивы. Но теперь она уже на самом деле не уникальна.
Неплохая статья про них уже была на хабре.
Не стану пересказывать все содержимое статьи, повторю всего одну фразу:
Директивы можно и нужно использовать для повышения модульности вашего приложения, выделения обособленной функциональности в компоненты, в том числе и для повторного использования.

У директив AngularJS по сути есть всего один большой, жирный минус: их невозможно использовать без собственно AngularJS, ты встаешь перед фактом, что нужно строить ng-приложение со свойственной ему структурой и разметкой.
Поэтому четыре месяца назад началась разработка решения, позволяющего создавать такие же отдельные элемент-специфичные задачи.

Как это работает?

Реализация работает на MutationObserver (для тех, кто не в курсе — родные события браузера на изменение DOM-дерева) с полифиллом, работающим на mutation events (DOMSubtreeModified и ему подобные). Полифилл реально нужен только для IE, так как все остальные десктопные браузеры уже поддерживают родной MutationObserver.
К сожалению, даже с полифиллом не поддерживается родной браузер android 2.3, что действительно печально, однако в 4.0 стабильно проходятся все тесты.
Теоретически «ручная» — с вызовом по необходимости проверки на обновление директив – поддержка директив возможна в практически любом мобильном и десктопном браузере, начиная с IE6.

Начало

Первоначально был определен синтаксис:

directive('name', function(node){
    alert("i'm alive!")
})


Вначале предполагалось сделать целевой элемент this, но из-за большого количества колбэков (перед которыми каждый раз приходилось писать var _this = this) решено было вынести его в первый аргумент.
Дальше синтаксис был расширен до еще одного варианта:

directive('name', {
    load: function(node){},
    unload: function(node){}
})


Событие load связано с появлением директивы(не элемента), unload соответствует ее удалению.
Почему именно с появлением директивы? Потому что их можно как прописывать изначально, так и добавлять и удалять в процессе работы с элементом. Простой пример — эта директива будет вызвана, если элемент изменится с
<div class="container something">

на
<div class="container test something">


Полезный this

Чтобы колбэки могли передавать друг другу значения – они получили общий this. Так что вполне возможно делать так:

directive('name', {
    load: function(){
    this.interval = setInterval(function(){}, 1000)
},
unload: function(){
    clearInterval(this.interval)
}
})


В некоторых случаях удобно создавать внешние методы для взаимодействия с содержимым: например, презентация с возможностью перехода. Для этого доступен сам скоп – он является одним из атрибутов ноды — так, для директивы test это будет node.directiveTest.
В итоге создание публичных методов для директивы становится простым и удобным:

directive('name', function(){
      this.publicMethod = function(a){alert(a)}
})


Синтаксис в HTML

В качестве целей для директив были заданы классы, атрибуты и тэги, и с самого начала предполагалась возможность использования префикса 'data-'(на самом деле в настоящий момент конфигураторе по умолчанию задан еще один префикс — 'directive-'. Это сделано для читаемости директивных классов: div class=«directive-scrollbox» куда понятнее чем div class=«scrollbox»).
Соответственно такая директива будет выполнена при следующих сценариях:

<div class="name"/>
<div class="data-name"/>
<div name/>
<div data-name="john"/>
<name/>
<data-name first-name="john" last-name="doe">


Работа с атрибутами

Решение передавать данные из атрибутов просилось само собой. Для директив в атрибутах был задан сценарий передачи значения атрибута, для директив-тэгов — формируется объект из всех атрибутов. В итоге в примерах выше в первом случае будет передано 'john', во втором — {'first-name': 'john', 'last-name': 'doe'}.
Для более «умной» передачи данных появилась поддержка «краткого» синтаксиса объекта: можно писать name=«first: 'john', jast: 'doe'». в действительности внутри происходит что-то вроде

try {value = eval(  '{' + value + '}' )}
try {value = eval(   value  )}
return value


Атрибуты могут меняться, и для их изменения тоже есть отдельный колбэк:

directive('name', {
    alter: function(){}
}


В некоторых случаях — например, для директивы вроде

directive('include', function(node, path){
    $.get(path, function(data){node.innerHTML = data})
})
 


действия на загрузке и изменении атрибута повторяются. Поэтому в случае, если не указано действие на load, оно автоматически берется из alter, что опять же позволяет уменьшить количество создаваемого кода.

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

Примеры

Из больших директив, которые уже используются в некоторых проектах, одна из самых приятных — это scrollbox, автоматическая «обертка» для любого элемента, которая навешивает на элемент кастомный скролл.

Пример на jsFiddle

А вот простой и удобный способ работать с drag-n-drop-ом файлов. Просто перетащите файл на серый квадрат.
Пример на jsFiddle
да, код немного объемен, но если добавить jQuery — он будет примерно в 2 раза короче.

А вот и повторение опубликованного пару месяцев назад на хабре решения для разработчиков. Для того, чтобы почувствовать всю увлекательность решения — нужно будет залезть в панель веб-разработки и вручную поменять значение repeat у любого из клонированных элементов. Если удалить атрибут — элемент станет одиночным, после чего можно будет добавить атрибут еще раз.
Пример на jsFiddle

Для тех, кто хочет попробовать cornerJS в своих проектах — минифицированная и обычная версии находятся тут:
Репозиторий cornerJS на GitHub
Uprock
0.00
Company
Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 24

    +8
    AngularJS весит где-то мегабайт

    Версия 1.2.0 RC2 весит ~89кб, это совсем не так много. Да и гугловский CDN намного улучшает ситуацию.
    Ясное дело, что в ангуляре синтаксис сумасшедший (касательно директив), но это только вопрос привычки. Да и в проекте, где потребовались директивы, скорее всего, потребуется и остальной функционал AngularJS.
      0
      Да, случайно ошибся когда вспоминал про ангуляр. Уже исправил, ответил ниже.
      0
      Симпатично. Скролл, правда, не айс — не для перетягивания мышкой, надо поправить. Но функционал приятный.
        0
        Спасибо. Не заметил, случайно взял старую версию директивы. Обновил ссылку на фидл, теперь все должно работать аккуратно
      • UFO just landed and posted this here
          0
          Насчет «меньше, чем jQuery» вы загнули (jquery.min.js ~32.5).

          UPD: Черт, фигню написал это же gzip.
            +1
            Да, с размером — наврал, извиняюсь, возможно, перепутал с какой-то другой библиотекой по памяти. Уже убрал.
            Но я ни в коем случае не хочу конкурировать с ангуляром, corner — это маленькая библиотека для вынесения всего того, что не связано с основным потоком работы приложения из него.

            Касательно синтаксиса — я ни в коем случае не критикую angularJS, я сам сделал на нем не один проект, вопрос в другом: он работает на durtyChecking-е. Да, он обещает перейти на Object.observe, но директив это не коснется, так как они в любом случае пересчитываются только при каждом ре-рендере шаблона.
            Более близкую аналогию с технологической точки зрения на самом деле можно привести с Polymer и принципом web Components, где с кастомными элементами можно ассоциировать определенный код, отсылку к директивам ангуляра я сделал скорее потому что это куда понятнее для объяснения. Ну и, конечно, потому что разработка началась благодаря ангуляру — после трех проектов на нем было очень сложно строить статику, а подключать такую-архитектуру на сайт, который не оперирует никакими данными, ощущалось немного глупо.
              0
              Ну в ключе не single-page приложений, смысл конечно имеется.
            0
            <del>
              +2
              1) angularjs весит меньше jQuery (ввели в заблуждение… давненько не смотрел на вес jQuery)
              2) так как в angularjs есть jqLite подключение jQuery остается на ваш выбор
              3) data-binding?
              4) Dependency Injection?

              последние 2 пункта так же экономят массу времени, и без них это как-то скучно… По сути все что есть в ангуларе сейчас — это как раз таки директивы и все что нужно для того что бы они работали (даже контроллеры подключаются через директивы).
              Все дополнительные фичи аля ngRoute и т.п. вынесено в отдельные модули.
                0
                нужно будет залезть в панель веб-разработки и вручную поменять значение repeat у любого из клонированных элементов

                Поменял у одного из элементов с «repeat=5» на «repeat=7», в итоге остался один элемент. Так и задумано?
                  0
                  Аналогично. Что-то я тоже не понял этот момент
                    0
                    Там ошибка, в консоль валится, поэтому «эффект» не работает. (Chrome 30)
                      0
                      Ого. Да, это мой баг — добавлял красивые эффекты к директиве для публикации, самое интересное — что у меня работало во всех браузерах.
                      Сейчас обновил фиддл, можно поиграться.
                  0
                    +1
                    Да, более того — в Corner используется полифилл для mutationObserver от полимера, и я сам пытаюсь отслеживать изменения обеих проектов.
                    brick указан зря — это комплект элементов на базе x-tags, в действительности существует всего 2 реализации web Components.
                    Я периодически использую в качестве эксперимента(например, там, где однозначно не будет поддерживаться ie) Polymer, однако он сам по себе несколько монструозен и тяжеловесен. В любом случае даже x-tags более тяжеловесное решение, в которое входят полифиллы для HTML imports, Custom Elements, входит достаточно больше количество кода, которое позволяет обсчитывать жизненный цикл компонента.

                    Если лезть глубже — то директивы Corner-а это что-то среднее между директивами ангуляра и компонентами.
                    С одной стороны — они навешиваются на элемент (не создают новый элемент, а базируются на старых), их может быть несколько, они могут появляться в любой момент жизненного цикла элемента и так же исчезать. С другой — их события действительно похожи на те, которые используются в web Components — решениях: создание, исчезновение, изменение атрибута.

                    В пользу директив говорит простота при почти том же функционале, в пользу веб-компонентов — переносимость(вы ведь в курсе, что и полимер, и x-tags способны использовать компоненты друг друга?). Однако далеко не в пользу компонентов говорит то, что они опираются на:
                    -WeakMap полифилл, при этом они используют решение, которое в в пользу скорости(прирост в сравнении с «правильной» реализацией огромный, в некоторых случаях — в сотни и тысячи раз в силу того что сложность официального решения O(n), сложность их — O(1)) несколько не соответствует стандарту и способно в некоторых случаях банально падать.
                    -MutationObserver полифилл, не способный отлавливать удаление атрибута(все остальное на самом деле полностью соответствует стандарту)
                    -HTML imports полифилл, который в силу своей природы выполняет синхронный XHR-запрос, что крайне негативно сказывается на времени загрузки страницы, и поэтому я бы рекомендовал использовать его только в разработке
                    -Shadow DOM полифилл. Его я не использовал, поэтому не могу по своему опыту ничего сказать, но то, что я видел в исходниках, наводит меня на мысль на то, что опять же быстродействие ощутимо проседает.

                    Полимер в действительности это огромный прыжок в будущее, и я нисколько не умаляю его роли, более того — я все собираюсь с духом написать про него большую и интересную статью, но все же… даже сами разработчики не рекомендуют использовать его в продакшне.

                    В любом случае, компоненты не позволяют расширять текущие элементы. А в случае же с Сorner-ом можно спокойно написать что-нибудь вроде

                    directive('input', function(node){
                        $(node).keyup(function(){
                            $(node).attr('value', $(node).val());
                        })
                    });
                    

                    и потом спокойно делать в SCSS примерно такой запрос
                    input {
                        &:active, &:focus, &:not([value=""]) {
                          & + .label {
                            display: none;
                          }
                    }
                    
                    –1
                    if ($(‘.element-carousel').length>0) {$('.element-carousel').initCarousel()}
                    if ($('.element-scrollbox').length>0) {$('.element-scrollbox').initScrollbox()}

                    Очень странный подход. Мне кажется надо было начинать с разбора этого кода, а потом уже про директивы.
                      0
                      Вполне стандартная ситуация, ничего странного.
                      Как по мне, в jQuery плагинах обязательно должна быть такого рода проверка внутри тех самых initCarousel и initScrollbox. Но мы с вами ведь знаем, что так бывает не всегда. Поэтому такой код можно часто встретить в проектах.
                        +2
                        обычно внутри плагинов код примерно такой:
                        $.fn.foo = function () {
                            return this.each(function () {
                                // bootstrap stuff...
                            });
                        };
                        

                        а это значит что проверка на количество элементов не нужна. Если не найдено ничего — то и пофигу.

                        Опять же можно сделать еще такое:
                        $('[data-plug]').plug();
                        как это сделано в том же twitter bootstrap.
                      +1
                      try {value = eval( '{' + value + '}' )}

                      OMG! JSON.parse забыли?
                        +1
                        Статья базируется на пастулате, что
                        function pageChange(){
                            if ($(‘.element-carousel').length>0) {$('.element-carousel').initCarousel()}
                            if ($('.element-scrollbox').length>0) {$('.element-scrollbox').initScrollbox()}
                        
                        это плохо.

                        Но нет решения этого плохого кода, с использованием Corner.js
                        Звучит как «Курение это плохо, а вот как мы решаем вопрос с алкоголем и наркоманией.»

                        Не понятно, почему вот это
                        directive('input', function(node){
                            $(node).keyup(function(){
                                $(node).attr('value', $(node).val());
                            })
                        });
                        лучше чем

                        $('input').keyup(function(){
                                $(this).attr('value', $(this).val());
                        });


                        Я не понял, чем хорош Corner.js. Чем решение на jQuery хуже или лучше.
                        Общий this — вообще нарушение основ языка, когда в jQuery это решается через .proxy, а в coffiscript через '=>'. Можно сказать что true = false, выдать за фишку библиотеки и ждать счастливого дебага.
                          +2
                          Видимо, я плохой автор, раз не смог объяснить.
                          Это работает не как jQuery. Это не альтернатива jQuery. Этот код работает через MutationObserver, который вызывает колбэки на каждое изменение DOM-дерева. И каждый раз, когда целевой элемент добавляется, отрабатывает целевой колбэк.
                          Я не представляю, как на jQuery можно красиво и быстро реализовать что-то подобное:

                          directive('include', {alter: function (node, path) {
                              $.get(path, function(responseText){
                                  node.innerHTML = responseText
                              })
                          }});
                          


                          При том что внутри одних шаблонов легко и непринужденно могут оказаться другие, иногда — даже не один раз.

                          А вообще статья базируется на постулате о том, что ПРЕДПОЛАГАТЬ появление новых элементов и вызывать инициализацию элементов по смене вьюхи или чего угодно еще это плохо.
                          Как угодно еще — это, например(реальный код, который я видел):

                          $(document).on('hashchange', function(){
                          setTimeout(function(){
                              $('.scroller').each(function(){
                                  initScroller($(this))
                              })
                          }, 500)
                          })
                          


                          При этом человек не первый день в вебе, далеко не первый, много запущенных и работающих сайтов.
                          То есть человек реально делает предположение, что через .5 секунды после смены текущего анкора вызовется смена вьюхи(которая делается тоже асинхронно в силу того что слушает то же самое событие), которая успеет обновить дом-дерево.
                          Что страшнее всего — сами вьюхи подгружаются через $.get.
                          В действительности же все скроллбоксы — сущности автономные, и держать их внутри основного кода — смысла нет. Соответственно — Corner это возможность вынести абсолютно весь код, который не относится к mainflow, в отдельные изолированные блоки.

                          А вот это
                          $('input').keyup
                          


                          будет работать только с теми элементами, которые в данный момент есть на странице. Если вы добавите еще один />, то он уже не будет обрабатываться.
                          Хотя да, без Corner заработал бы такой код. Не стану спорить.

                          $('body').on('keyup', 'input', function(){
                                  $(this).attr('value', $(this).val());
                          });
                          


                          Относительно общего this.
                          Если бы я предложил синтаксис
                          directive('test')
                          .onload(function(node){this.name = node.innerHTML})
                          .onunload(function(){alert(this.name)})
                          

                          вам было бы комфрортнее принять общий this? Потому что jQuery предлагает именно такой синтаксис. Я просто пришел к тому, что цепочка вызовов, в каждом из которых this ссылается на ноду — не так удобна для декларирования, чем общая конфигурация сразу же.
                          0
                          Будет ли это работать с jQuery? Общий this только внутри directive?
                            0
                            общий this — у всех колбэков, это node.directive%имя директивы с заглавной буквы%. т.е. для директивы test это node.directiveTest.

                            с jquery — без проблем, только нужно оборачивать в $(node), передается родная нода, не жкверишная.

                          Only users with full accounts can post comments. Log in, please.