MaskJS — HMV* фреймворк


    Разрабатывая MaskJS вот уже больше полугода, удалось превратить DOM шаблонизатор в очень мощный, но при этом производительный веб фреймворк. В статье познакомлю вас с возможно интересными подходами к разработки. Уверен, будет интересно почитать о использовании сигналов и слотов вместо DOM событий. И как компоненты делают нашу жизнь проще. Маска легко интегрируется в уже готовый проект, и даже может быть использована вместе с любым другим фреймворком. Основным же отличием наверное является render flow, где в процессе поэтапно создается Document Fragment / контроллеры / «биндинги». Собственно всю гибкость даже сложно передать, но я попробую, и приглашаю под кат.


    MaskJS@GitHub
    Небольшой todo пример для разогрева. mask-fiddle лучше всего работает на webkit-е

    View



    Разметка


    На случай, если кто-то не знает, в шаблонах используется синтаксис схожий с css/less/sass. За последнее время были исправлены различные баги, поэтому работа движка должна быть теперь стабильной.

    p {
        div#info.dark > 'Single Child'
        button data-user='123' style='cursor:pointer;' >  span > 'Submit'
        input type=hidden value=x;
    }
    

    Как видно, мы убрали "<>" из тэгов и убрали закрывающие тэги, а вместо этого блоки выделяются привычными "{}" скобками. (Для простоты блок с одним ребёнком похож не селектор с ">" переходом, а вовсе без детей — закрывается точкой с запятой). Текст же помещается в литералы, как в javascript. Вот таким не хитрым преобразованием, мы легко сконцентрировали разметку на структуре — что собственно более востребовано в архитектуре приложения, чем избыточность html, который нацелен на разметку текста. Многих приверженцев html это удивляет, но давайте посмотрим правде в глаза — мы, и наверное также как и вы, разрабатываем приложения с множественной локализацией. Текста в представлениях у нас нету — только ключи к json с локализацией, зачем тогда спрашивается синтаксис гипертекстовой разметки?
    А в отличии от шаблонов основанных на отступах, маска легко минифицируется и занимает минимум места.

    Скорость


    Это является основным приоритетом в разработке MaskJS — что бы предлагать максимальную производительность на мобильных устройствах. Удалось добиться скорости более-менее сопоставимой с html, a в случае с webkit движком — даже увеличить, особенно это относится к мобильным платформам. И не потому что html parsing в webkit-ах медленный, а потому, что str.charCodeAt и document.createElement очень быстрые ). И главное, накладные расходы на контроллеры / интерполяцию / dom события / data bindings в архитектуре MaskJS минимальны. В результате, нам больше не нужно компилировать шаблоны, а это уже большой плюс к наслаждению от разработки. Если интересно, несколько ссылок на jsperf.com найдете в readme на гитхабе.

    Гибкость


    MaskJS — довольно расширяемая система. Мы можем определять контроллеры к любым тегам и создавать новые, собственно вся иерархия контроллеров(HMVC) строится на этой фиче. Можем определять обработчики к любым атрибутам. Также мы можем определить утилиты, которые, при интерполяции модели в шаблоне, будут трансформировать или переопределять данные. И напомню, что маска на клиенте рендерится непосредственно в DocumentFragment, поэтому мы всегда работаем с DOM элементами.
    Все контроллеры создаются через подобие IoC контейнеров, а если вы «в теме», то сами понимаете, как легко будет их переопределять или имитировать («мокать»).
    У вас есть jQuery widget (или аналог) и вы устали каждый раз его инициализировать после вставки в дом?.. Например, пришел ответ от серверa, используя любой другой шаблонизатор — вы создали представление, вставили в DOM, а потом ещё прошлись по нужным элементам и проинициализировали widget-ы. С MaskJS вы создаете Тэг-Обёртку над вашим виджетом, и маска сделает всё за вас:
    mask.registerHandler(':timer', Compo({
       // пример шаблона для виджета
       template: '.cotainer > .someInnerPanel',
       slots: {
           domInsert: function(){
                 // этот слот будет вызван после вставки в "живой дом", 
                 // на случай, если нужно производить дом-зависимые расчёты
           }
       }
       onRenderEnd: function(){
               this.$.mySuperTimer({ timespan:  this.attr.timespan << 0 });
               /** 
                   Важный момент - значения атрибутов это строки, если только значение не было
                   интерполировано с модели / контроллера, тогда значение может быть как любым значением,
                   так и любым экземпляром класса.
    
                  В данном примере использую левый сдвиг для преобразования строк в целое (int) число, а undefined в ноль.
              */
       }
    });
    
    // = шаблон
    // ...
    :timer timespan="5000";
    


    Теперь, можно хоть сколько угодно этот тэг использовать в шаблоне, а вот инициализировать больше не нужно. Небольшой пример создание таймеров:

    $.getJSON(url).done(function(collection){ 
        jmask("ul > % each=timers > li > :timer timespan='~[timespan]'; ", collection).appendTo(document.body);
    });
    


    Design Patterns



    Существует множество разных архитектурных решений, но у всех есть общая цель — уменьшить связи и зависимости. В MaskJS основной акцент делается на V(View) из MVC, и мы пытаемся абстрагироваться от Модели. Маске не важно, как выглядит ваш Business Layer и откуда он «берётся». А это значит, что все классы, данные и любая бизнес логика независима от представления и контроллеров — и не только архитектурно, но и от MaskJS библиотеки в целом. Модель может быть как Data Centric (прим. — json service response), так и комплексным Domain Model. Но в любом случае она отделена и тем самым проста для разработки и тестировании.
    Далее приведу маленькие примеры разных сценариев MVC, кое что будет утрировано — так что не судите строго, делаю это только в целях лучшей наглядности.
    • Вот у нас View:
      mask.render(" div > 'A' ");

    • Добавим динамичности, отобразив букву из модели ( var model = { letter: 'A' } ):
      mask.render(" div > '~[letter]'  ", model) 
      — получаем (Data)Model / View
    • Свяжем модель с представлением так, что бы при изменении буквы в моделе — представление обновлялось:
      mask.render(" div > '~[bind: letter]' ", model);
      — вот уже и Model / View / ViewModel
    • Если нам надо изменить или дополнить данные для представления — получим Model / View / Adapter:
      mask.registerHandler(':myModelAdapter', {
          renderStart: function(model){
              _extendModelFromLocalStorage(model);
         }
      });
      
      mask.render(" :myModelAdapter > div > '~[letter]' ", model);
      

    • Если нам надо отделить view от модели получаем Model / View / Presenter
      mask.registerHandler(':myPresenter', Compo({
              onRenderStart: function(model){
                      this.letter = _handle(model);
                      this.model = this;
              }
      });
      
      mask.render(" :myPresenter > div > '~[letter]' ", model);
      

    • Если надо, что бы буква изменялась на “B” при клике – привет Model / View / Controller:
      mask.registerHandler(':letterChanger', Compo({
      	events: {
      		'click: div' : function(event){
      			this.model.letter = 'B';
                              // если не испольовать биндинги - должны обновить представление сами
                              // this.$.text(‘B’);  /* this.$ = jQuery/Zepto wrapper */
      		}
      	}
      });
      mask.render(" :letterChanger > div > '~[bind: letter]'  ");
      

    • Если надо дополнить модель + реагировать на клик – и вот уже иерархия – HMVC
      mask.render(':myAdapter > :letterChanger > div > “~[bind: letter]" '); 

    • Если надо скрыть представление в контроллер, получаем обычную инкапсуляция — (это конечно не архитектурный шаблон проектирования, но очень важный момент в MaskJS)
      mask.registerHandler(':letter', Compo({ 
          template: ":letterChanger > div > '~[bind: letter]' "
      });
      mask.render(" :myAdapter > :letter; " , model);
      


      А вы знали — что для полной мощи инкапсуляции не плохо пользоваться различными загрузчиками, тем самым выносить контроллеры, их представления и стили в отдельные файлы. Простой пример композиции компонент:
      header {
          #logo;
          :menu;
          :userInfo; 
      }
      :viewManager {
         :userView;
         :aboutView;
      }
      
      :pageActivity;
      :notifier;
      
      :footer;
      

      Названия компонент начинается двоеточием только для лучшей семантики представлений.


    Но главное в этих всех MV* — не их названия, тем более здесь все притянуто за уши (надеюсь никого не обидел?). А сама суть, то как мы создаем контроллеры разного назначения. И как видите, зависимости мы указываем непосредственно из представления — этим самым разгружая сами контроллеры и оставляем их заниматься только своими непосредственными задачами.

    Component / (Controller) / (Widget)


    AST

    MaskDOM

    «Парсер» трансформирует View в дерево «нод». По нему потом проходится «билдер» и интерполирует модель — создает HTMLElement-ы и Контроллеры (произвольных тегов). В стандартную сборку MaskJS входит ещё одна хорошая библиотека — jmask@github. Она помогает работать с maskDOM деревом, в ней используется синтаксис jQuery и её удобно использовать везде, где нужно динамически создавать maskdom дерево или изменять его, например в onRenderStart компоненты:
    //..
    onRenderStart: function() { 
        jmask(this).tag('div').addClass('pixel').wrappAll('.container data-id=dialog');
        //  eq. == jmask(this).wrappAll('.pixel > .container data-id=dialog');
    }
    

    Если где-то вы используете jQuery для создания DOM, то маска справится с этим точно также, и причем в разы быстрее, маленький пример
    $('<div><span></span></div>').addClass('container').data('foo','bar').children('span').text('2013').appendTo('body')
    
    // то же с jmask
    jmask('div > span').addClass('container').data('foo','bar').children('span').text('2013').appendTo(document.body)
    jmask('.container foo=bar > span > "~[text]"').appendTo(document.body, { year: 2013 })
    


    Controllers Tree

    Билдер также создаёт дерево из компонент, поэтому можно через селекторы находить другие контроллеры
    mask.registerHandler(':page', Compo({
        // ...
        foo: function(){
              // ...
              // функция find вернёт первый найденный компонент
              this.find(':scroller').scroller.refresh();
              this.closest(':item[data-id=5]]').bar();
    
              // через jmask можно находить все контроллеры
              jmask(this).find(':listItem').each(function(x) { x.bar() })
        }
    });
    



    Signals / Slots


    mask-compo@github

    Компонент может иметь хэш объект с перечнем всех событий которые хочет обработать —
     
    var _myCompo = Compo({ 
        constructor: function(){ this.name = 'C'; }, 
        events: {
            'touchstart: .pane': function(event){ this instanceof _myCompo // -> true }, 
            //...
        }
    });
    

    Но таким образом, мы привязываемся к разметке(css классам) — а это значит — привязка к реализации представления, что усложняет нам замену View. И это не есть хорошо. Во многих фреймворках можно вызывать методы контроллера непосредственно из представления, но это тоже не дело, хотя MaskJS поддерживает expressions в шаблонах — div > '~[: controllerMethod("test") ]' (замечу, что в маске реализован свой expression parser и evaluator без with/new Function /eval). Намного лучше, когда представление посылает сигналы вверх по дереву контроллеров, начиная с «владельца» элемента — а там уже, кто хочет, тот реализовывает логику.
     
    mask.registerHandler(':myCompo', Compo({ 
        constructor: function(){ this.name = 'C'; }, 
        slots : {
            greet: function(sender){ 
                // sender в контексте dom событий это сам event object, иначе компонент пославший сигнал
                alert(this.name); // "C"  
                // return false - на случай, если нужно остановить передачу сигнала дальше по дереву компонент вверх.
            }, 
            //...
        }
    }));
    mask.render(" :myCompo > .panel x-signal='click: greet'; ");
    

    Заметьте декларативное объявление слотов в объекте slots — этим самым, мы чётко разделяем логику контроллера, а маска сама вызовет эти обработчики, когда соответствующие сигналы будут запущены.
    Дополнительно, мы сможем в любой момент слот или сигнал деактивировать, и при этом, все элементы, которые посылают этот сигнал в данной «области видимости» контроллеров, получат статус :disabled.

    Pipes

    Обычные сигналы гуляют только вверх или вниз по дереву компонент, и что бы связать два компонента, которые лежат вне иерархии, нужно использовать трубки.
    mask.registerHandler(':userInfo', Compo({
        pipes: {
            // pipe name
            user: { 
                logout: function(){
                     this.model.authenticated = false;
                }
            }
        }
    }));
    
    // = template.mask =
    menu { 
        #logout x-pipe-signal='click: user.logout' > button > 'Sign out' 
        // ....
    }
    section #content  {
          // возьмём объект "user" из модели и передадим нижнему шаблону 
         % use="user" > :userInfo type='brief' ;
         // ..
    }
    section #footer {
        // bind to "user.authenticated" prop
        %% if="user.authenticated" > % each="user.keys" > "~[.]"
    }
    

    В этом примере попытался немножко усложнить представления.

    Все сигналы также можно посылать из самих контроллеров:
       this.emitIn('name', args...);       // детям
       this.emitOut('name', args...);    // родителям
       
       Compo.pipe('user').emit('logout'); // начинаем с последнего контроллера присоединившегося к "трубе".
    


    Bindings


    mask-binding@github
    Как же веб фреймворк без «привязок»? Здесь всё в лучших традициях жанра: One- / Two-way Bindings, Custom Binding Providers, Array Mutators, Validators.
    Пример можно посмотреть на mask-try | bindings. Биндинги по своей природе очень производительны, так как в render time они только сохраняют ссылки на дом элементы, и привязываются к моделе через defineProperty/ __defineSetter__. И да — вы правильно заметили, старые браузеры не поддерживаются — но переопределив стандартный провайдер, можно добиться привязки к функциям вида setX/getX, или другим шаблонам, как .get("x")/.set("x"). Собственно, если нужно, можно ограничения обойти.
    Интересные моменты:
    • можем использовать выражения, пример:
       div style='height: ~[bind: item.age * index + 10]px' 
      В зависимости как будет меняться возраст или индекс — такой будет высота панели div
    • двух-направленное связывание основанные на dom events / (jquery) custom events / сигналах.
    • для того, что бы модель была целостная, двух-направленные привязки могут иметь компонент(ы) :validate. Перед тем, как присвоить значение модели, провайдер вначале проверит его, а в случае ошибки сообщит об этом пользователю и предложит вернутся к последнему верному значению. Так наша модель остаётся целостной.
      
      input #device-type type=value > :dualbind value="age" {
          :validate match="^[a-z]{2}-[\d]{4}$" message=" ... pattern: xx-1234"
      }
      

    • как вы уже может заметили маска имеет стандартный компонент %, который реализует логику if/else/each/repeat/use и прочее. Так вот, в модуле bindings реализован также one-way binding для этих вещей:
      %% if="state == true" { 
             %% each=userList {
             // ... template
      
      




    Разработка

    Важно не только писать большие и производительные приложения, но и получать от этого максимум удовольствия. Разделив разработку на компоненты, начиная от самых маленьких(:customCheckBox) до самых больших(:inbox), мы всегда концентрируемся на необходимом. Что бы отловить баги, в системном контроллере есть атрибуты debugger и log:
    .user {
         % debugger;
          .user-status > '~[bind:info.status]'
         %% log="info.status";
    }
    

    • debugger — во время render flow мы остановимся и можем посмотреть стэк компонент, текущую модель и html-элемент.
    • log — выводим в консоль данные. Можем доступиться как к модели, так и контроллеру.

    Hot Reload Plugin

    … для IncludeJS
    «Горячим» обновлением ресурсов без перезагрузки страницы сейчас никого не удивишь — да и реализация довольно тривиальная, но со скриптами не всё так просто. Здесь должны учитываться замыкания, dom события и прочее. Мы используем IncludeJS библиотеку для загрузки всех модулей, и каждый скрипт файл может экспортировать метод reload. Также в состав IncludeJS.Builder-а входит сервер, который следит за изменения запрашиваемых файлов и через socket.io оповещает IncludeJS. Собственно сценарий довольно простой. А вот MaskJS в свою очередь, в reload плагине переопределяет mask.registerHandler — в нем он записывает все компоненты которые регистрируются и соотносит их к пути текущего скрипта. Также плагин подписывается на событие создания контроллера, и сохраняет текущую модель и ссылку на контроллер. Таким образом, когда через socket.io мы получим оповещение о изменения файла, у нас есть список названий компонент, которые этот файл создаёт, а также список экземпляров(instances). И далее дело техники — вызвать remove/dispose каждого контроллера, и проинициализировать на их места обновлённые компоненты. Используя сигналы и слоты, родителям не нужно подписываться на dom события обновлённых компонент — сигнал и так дойдёт. Если компонент загружает mask markup отдельно и мы что-то в нем изменили, тогда IncludeJS будет расценивать это, как изменение в самом customCompo.js файле. Тема IncludeJS довольно ёмкая и об всех его возможностях как-нибудь в другой раз. Но то, что архитектура MaskJS позволяет заменять компоненты на лету, сильно упрощает разработку, особенно, если компонент спрятан за N кликами(прим. где-то в диалоге).

    Node.js и TODO


    На данный момент MaskJS также работает в node.js. Принципы работы те же, только после создания mask dom, создается html dom, которое в свою очередь превращается в string buffer. А на клиенте все компоненты будут проинициализированы и для завершения получат нужные DOM элементы в onRenderEnd методе.
    Маршрутизация завязана не на контроллерах, как во многих фрэймворках, а на представлениях. Помните? Представление само инициализирует нужные контроллеры. Тут же можно использовать Master Pages техники и прочее.
    Работа в этой области ещё не закончена, нужно будет ещё разобраться с некоторыми нюансами. Но целью является то, что бы компоненты/контроллеры / виджеты работали как на клиенте (в первую очередь), но и с возможностью рендеринга на backend-е с завершением на клиенте.
    Хочется ещё создать обертку из компонент над каким нибудь css фреймворком. Маска упростит разметку в разы — спрячем css классы и div wrappers ). И упростим создание меню/календарей/диалогов и т.д.

    FIN

    Это пожалуй все основные моменты. Многое ещё не рассказал, но надеюсь было понятно хотя бы то, о чём шла речь, ведь с меня рассказчик никакой, а материала много, поэтому сложно сконцентрироваться.
    В комментариях, пожалуйста, не пишите — «посмотри на ИКС фреймворк!» — мы следим за мейнстримом, а вот более глубоким комментариям, буду очень рад.


    Хотел бы ещё поблагодарить хабраюзара rma4ok за многие дельные советы. Многое попытался учесть. Также буду рад любым другим советам или пожеланиям. Если вы знаете интересные техники из других языков / фреймворков — пожалуйста, поделитесь знаниями ). А также можете присоединиться к разработке.

    Удачи.
    Поделиться публикацией

    Комментарии 33

      +4
      Как насчет примера Todo на todomvc.com/?
        0
        Да я раньше туда отправлял реализацию, но тогда это был лишь шаблонизатор без разных «наворотов» и они приняли только как альтернативу. Сейчас я обновил приложение и может попробую ещё раз послать «request». Приложение здесь. Или более длинный путь:
        • установить nodejs / git / inlcudejs / libjs
        • > cd tempFolder && ijs template todoapp
        +1
        Отличная идея, но реализация, по-моему, неудачная, как и у всех других подобных проектах, которые в последнее время появляются в огромном количестве.
        Что лично мне не понравилось:
        1.
        #logout x-pipe-signal='click: user.logout' > button > 'Sign out'
        или
        '~[bind: date.getDate() ]'
        Вы тут предполагаете, что свойства «user»/«date» в некой js-модели будут всегда называться именно так. Т.е. при общей декларативности фреймворка вы привязываетесь к js-объектам.

        2. Примеси/наследование компонентов непонятно как должно работать (должно ли вообще).
        Т.е. в вашем примере я не смог «подмешать» компонент :customTag в конец компонента :datePicker, он постоянно оказывался вверху скопа. Возможно у вас есть какой-то другой синтаксис для этого?
        td > :datePicker {
              р4 > 'test'
                    
              :dualbind value='date' getter='getDate' setter='setDate';
                    
              :customTag > button x-signal='click: clicked' > 'Click';
        }
        


        3. Опять же, несмотря на декларативность фреймворка, вы привязываетесь в DOM дереву. Лично меня это всегда отпугивает от подобных декларативных фреймворков, потому, что, при желании, view backend можно писать и на WebGL/canvas.

        4. Синтаксис избыточен. Т.е. вместо
        td > div > '~[bind:name]';
        можно было бы сделать
        td > $name
        div -тег по-умолчанию и его можно опустить. Префикс $ — указание на то, что данный «узел» является свойством модели.
          0
          Дельный комментарий, спасибо — теперь по-пунктам:
          1. Сигналы — это не вызовы методов, а как бы именованные события, которые по дереву контроллеров или трубкам гуляют. А привязываться к модели из шаблонов вы как предлагаете по другому? Эту задачи в любом случае нужно кому-то делегировать. Если мы из конроллеров будем вставлять в дом данные — тогда контроллеры слишком толстые получатся. Здесь же шаблоны отвечают за свои задачи — вывести данные пользователю, а в случае сигналов — предают динамичности посылая «события». Мне кажется такое распределение вполне уместно. Или есть у вас другие идеи?
          2. :datePicker, это всего лишь обёртка над jQuery виджетом. И её реализация не подразумевает, что вы дополнительно в календарь свою разметку вставлять будете. Поэтому он просто это игнорирует. datePicker
          3. В отличии от других реализаций, разметка превращается в AST дерево. И реализовав свой билдер можно будет и на canvas рисовать
            rect dimension='0:0:100:100' color='red' {
                line from='0:0' to='5:5' size=5;
            }
            

            Ну смысл вы уловили.
          4. Вы убрали div — а если нам надо, что бы данные выводились в span? Сейчас data биндинги это лишь надстройка над MaskJS (не путать ~[name] и ~[bind: name]]), поэтому и синтаксиса специально для него нету. И из вашего примера, так будет через чур свободно, или?
            td > p > 'My name is $name'
            // vs
            td > p > 'My name is ~[name]'
            

            Хотя да, смотрится не плохо, но не зря все шаблонные движки выбирает немножко посложнее синтаксис для интерполяции.

            0
            2. Можно ввести ключевое слово для вывода контента по-умолчанию. Например __yield__:
            td > :datePicker {
            р4 > 'test'

            :dualbind value='date' getter='getDate' setter='setDate';

            __yield__

            :customTag > button x-signal='click: clicked' > 'Click';
            }

            __________

            Или есть у вас другие идеи?

            Как я себе это представляю:
            1. Каждый компонент содержит набор свойств и модель по-умолчанию. Доступ к свойствам осуществляется через прослойку (backend). Для HTML, свойства — это DOM узлы. У каждого свойства есть шаблон по-умолчанию, это может быть просто вставка в innerHTML или аттрибут (src для тега img) или что-то сложнее.

            2. К компоненту может быть привязана кастомная модель, которая переопределяет доступ к одному или нескольким свойствам, или переопределять шаблоны валидации всей модели или отдельных свойств.

            3. У каждого компонента есть контроллер по-уломчанию, который может быть заменён или расширен кастомным конроллером.

            4. Контроллеры компонента не имеют доступа к DOM API и не подписываются на DOM события.
            Фреймворком должен быть определён набор сандартных событий в которые будут транслироваться DOM события, типа «active:start» (mousedown, touchstart), «active:end» (mouseup, touchend), «hover:start» (mouseenter), «hover:end» (mouseleave), «action» (click, submit) и т.д.
            Обработчик события можно повесить только на свойство компонента.

            5. Каждое свойство — это некий узел (Node для HTML backend) который может отлавливать события. При возникновении какого-либо события, класс с таким же именем устанавливаться/снимается на узел привязанный к свойству — Например, node.classList.add(«active») для «active:start» и node.classList.add(«remove») для «active:end».

            6. К компоненту может быть привязан источник данных. Это может быть как и просто массив, так и класс имплиментирущий абстрактный интерфейс ModelDataView с методами получения/записи/фильтрации данных.

            7. В модели описано, какие узлы array-like и какие одиночные, поэтому при применении данных на шаблон, array-like автоматически склонируются, и не нужно никаких each в коде шаблона

            8. В шаблоне, на один узел может быть повешено более одного компонента.

            Абстрактный шаблон компонента ImageGallery
            используется псевдо-HTML
            = section :ImageObject extend :MediaObject # '= ' - объявление компонента
                p ::name
                image ::contentURL
                body
                    p ?::author # '?' - узел будет создан только если свойство присутствует в источнике данных
                    data ?::datePublished
            
            = section :ImageGallery extend :CollectionPage
                header
                    hx ::name
                    p ?::description
                    image ?::image
                body #
                    ::primaryImageOfPage[ :ImageObject ] # свойство суб-компонент типа ImageObject
                footer
                    ::associatedMedia[ :ImageObject ] # превьюшки, суб-компоненты типа ImageObject
            



            Контроллер
            class ImageObject_controller extend MediaObject_controller {
                constructor(el) {
                    super(el);
                
                    this.model = _.getModel(el); // _ - глобальная переменная фреймворка
                    this._el = _(el); // обёртка над Node элементом (в случае html backend)
                }
            
                openImage(imageObject = 0) {
                    if ( Number.isInteger( imageObject ) ) {
                        imageObject = this.model.associatedMedia[ imageObject ];
                    }
            
                    if ( !imageObject ) return;
            
                    this.model.primaryImageOfPage = imageObject; //тут вызывается setter у модели
                }
            
                events: {
                    "associatedMedia action": function(event) {
                        let {target, metaKey} = event; // деструкруризация let target = event.target, type = event.type
                        
                        if ( !metaKey ) {
                            this.openImage(target);
                        }
                        else { // Нажали Ctrl + click - открыти картинку в новом окне
                            let model = _.getModel(el)
                                , url
                            ;
                                            
                            if ( model && (url = model.url) ) {
                                _.globalRouter.openInNewWindow(url);
                            }
                        }
                    }
                }
            }
            



            Модель
            class ImageObject_model extend MediaObject_model {
                constructor(dataView) {
                    super(dataView);
                    
                    this.dataView = dataView;
                }
            
                get associatedMedia() {
                    return this.dataView.get("associatedMedia");
                }
            
                set associatedMedia(singleOrArray) {
                    if ( !_.validate("associatedMedia", singleOrArray, this.validation ) {
                        throw new ValidationError(_.validate.lastError);
                    }
                
                    if ( Array.isArray(singleOrArray) ) { // Если массив значению
                        return this.dataView.replace("associatedMedia", singleOrArray); // заменяем всю коллекцию
                    }
                
                    // Если один объект
                    return this.dataView.add("associatedMedia", singleOrArray); // то добавляем его вколлекцию
                    
                }
                
                // ... остальные свойства
                
                validation: { // Валидация
                    ":scope": function() { // ":scope" ссылка на "всю" модель
                        // ...
                        // валидация всей модели
                    }
                
                    "associatedMedia": function(obj) {
                        return obj.contentURL && obj.name;
                    }
                }
            }
            



            Применение в шаблоне
            !!!
            html
                head
                    js 'script/something.js'
            
            body.image-gallery :ImageGallery /images/get/92188?from=0&to=10 # /images/get/* ссылка на источник данных
                # Устанавливаем css класс и текст по-умолчанию
                .image-gallery__author ::author ? "Автор не указан"
                
            



            Прошу прощения за длинный комментарий
              0
              Хорошие комментарии длинными не бывают, даже если занимают 10 скроллов)

              • С :datePicker-ом вы делаете, что то не то) Если вам надо до или после календаря вывести текст / кнопkи, так не обязательно помещать их в блок :datePicker —
                   h4 > 'Title'
                   :datePicker;
                  button > 'X'
                

                • Шаблон — p ::name — у вас также шаблон зависит от модели, а если имеется ввиду, что это данные из контроллера — тогда контроллер, это чистой воды Presenter — который в свою очередь получает данные из модели. (см. статью)
                  Наследование тоже самое что и MasterPages — layout
                • Контроллер — не понятно, как у вас определяются события. На узлы (DOM HTMLElements) всех компонент вешаются события клик (action)? А если надо не на весь элемент вешать обработчик, а только на кнопку 'OK' например, а если много кнопок?

                  Наследование контроллеров в MaskJS устроено пока что, только как обычное наследование Javascript прототипов — а там уже мы можем, например, переопределить метод onRenderEnd подсунув там другой шаблон для компоненты.

                • Модель — точно также можно и в маске это сделать. Но мне нравится, что не обязательно!! создавать врапперы — маске подойдет любой объект.



              На счёт array-like — исходить из того, что фреймворк и так догадается, не особо стоит, потому что порой мы передаём массив, но хотим, что бы не элементы выводились из него, а к примеру, только статистика по нему. И если у вас есть модель, где не только массив, а помимо и другие пропертя, поэтому нам всё равно нужно передавать его (массив) в шаблон / контроллеры — почему бы не через each?

              Ну а так расширить MaskJS до вашего видения не составит труда — я попытался по максимуму упрастить всё — и по максимуму дать возможность расширять. И если у вас есть время, можете форкнуть и попробовать поиграться — а если будут вопросы, я вам помогу.
          0
          Чтобы мне долго не разбираться (идея навскидку неплохая, но времени сейчас нет) — пробовали ли сочетать с Backbone.js? Раз уж за мейнстримом следите.
          Скажем, Backbone мне по идеологии нравится значительно больше аналогов, но по части работы с DOM он безнадёжно отстал от жизни (из основного: нет встроенной системы байндинга, нет компонент). Если использовать Backbone для роутинга/фетча а Mask для отрисовки, много ли придётся дорабатывать напильником? Скажем, обращение к атрибутам модели там только через параметризованные геттеры: model.get( «someAttr» ). Mask это сможет обрабатывать, включая отслеживание изменений?
          И да, плюсую по предыдущему комментарию: синтаксис шаблонов действительно избыточен.
            0
            Да я видел, как люди использовали маску с бэкбоном, используя её как шаблонизатор. И постепенно создавали свои компоненты (обработчики тэгов). Биндинг в Маске реализoван привязками через defineProperty — согласитесь, иметь в конечном результате "model.someAttr" лучше, чем model.get("someAttr");. Если вы получаете данные по ajax запросу, вам их не надо расширять до бэкбоновской модели — а напрямую передавать во View.
              0
              В Backbone это именно модель, а у вас — данные или состояние.
                0
                Это плохой вариант, в Backbone на модели завязано очень многое, в том числе и фетчинг. То есть фетч там забирает данные сразу в модель, а вы мне говорите, не используйте бэкбоновские модели. Всю работу с серверной частью вы предлагаете вынести в голые Ajax-запросы? Во-первых, это шаг назад, во-вторых, от бэкбона тогда остаётся только роутер, то есть это никак не тянет на «использовали маску с бэкбоном». Да и использовать бэкбон только ради роутера смысла нет, роутер можно и отдельно найти.
                В общем, советую пересмотреть концепцию: пока получается, что на серьёзных проектах использовать «голую» маску большого смысла нет, так как она не решает значительной части актуальных задач, а гибкости для сочетания с фреймворками, которые эти задачи решают, не хватает.
                  0
                  Да, Маска — model agnostic. И это к лучшему — модель может выглядеть как угодно — будь это данные из mongoose в node.js, или данные из localStorage, или обычный класс User на клиенте. Плюс для тестов это на порядок лучше.
                  Хотя маска и поддерживает такую запись — div > '~[:get("username")]', что бы достать данные из модели — но биндинги на данный момент работать не будут таким образом.
                    0
                    Так где же «модель может выглядеть как угодно», если вы мне ответом выше сказали «не используйте бэкбоновские модели», и здесь говорите, что байндинги с ними работать не будут?
                      0
                      Вопрос-то очень простой: если не использовать бэкбоновские модели, тогда зачем бэкбон? Если не использовать байндинги, тогда зачем маска (односторонних шаблонизаторов для бэкбона пруд пруди)?
                      Я поэтому и спросил, можно ли использовать вместе. Получается, фактически нельзя.
                        0
                        Понимаете, model.get('attrName') — это ужасный хак. Проперти должны оставаться ими, а не создаваться функции геттеры / сеттеры. Биндинг в маске это всего лишь плагин, который может привязаться к свойствам любой модели model.attrName. Но как уже сказал в статье, можно создать кастомный биндинг провайдер, который будет привязываться к этим функциям и слушать изменения. А такая запись div > '~[:get("username")]' — это не binding, а просто expression.

                        А ответ на вопрос довольно прост — backbone и не нужен. Нормальных Data Fetch / Routings библиотек тоже пруд пруди. Маску же можно использовать в контексте backbone как шаблонизатор для более плавной миграции от бэкбона к маске в целом. В результате получите более целостное и более быстрое приложение.
                          0
                          Ну позиция понятна, но мигрировать с бэкбона на маску я точно не собираюсь. Если бы хотел мигрировать, выбрал бы Angular, но я уже обозначил выше, что идеологически бэкбон мне намного ближе.
                          В качестве шаблонизатора с байндингом/компонентами поюзал бы, но нет так нет.
                            0
                            Angular ничем не лучше, а в производительности очень отстаёт, особенно в контексте мобильных девайсов, где у всех webkit. jsperf — одно и тоже приложение. А если вы сравните с бэкбоном, то тоже думаю удивитесь. Кстати, не хотели бы создать такое же маленько todo на бэкбоне?
                              0
                              А смысл в этих todo? Они ничего не показывают. Хороший фреймворк для написания todo != хороший фреймворк для написания сложных приложений.
                              Приложения — это во-первых, данные, во-вторых, бизнес-логика. И только в третьих — представление. Именно поэтому все UX-центричные фреймворки я недолюбливаю. В качестве дополнения к Backbone (где в центре внимания данные и логика) — да, хорошо. В качестве замены — нет, спасибо.
                                0
                                Это очень некорректный тест, которым вы вводите людей в заблуждение. Angular не шаблонизатор и замерять «холодный старт» совершенно не верно. Максимум можно сравнивать выполнение `$scope.$apply()`.
                                  0
                                  А можете более подробно сказать, почему тест не корректный — это две идентичные программы — после теста можете вставить тестируемые участки в консоль, и убедитесь, что все биндинги и прочее работает. И маска здесь выступает не только как шаблонизатор…

                                  Поэтому, я исхожу из этого, что вполне корректные тесты — на выходе дают один и тот же результат…
                                    0
                                    Нет, сравнивать «запуск» Angular неправильно, он происходит один раз на domReady и всё, так что нужно сравнивать именно реакцию на изменения данных/модели, т.е. замерять скорость обработки биндинга. В теории, у вас должно быть быстрей, т.к. используете getter/setter, в то время как у Angular dirty check, вот примеры более менее грамотных тестов, хотя и не совсем:
                                    angularjs-vs-knockoutjs
                                    angular-vs-knockout-vs-ember
                                      0
                                      И ещё один вопрос, а вы группируете манипуляции с DOM, если одновременно меняются N количество моделей?
                                        0
                                        происходит один раз на domReady
                                        — я же как раз и сравниваю этот «запуск» приложения — на сколько быстро angularjs сможет приложение зарендерить и создать биндинги — и не важно где происходит инициализация участка страницы — в domReady или после ответа сервера.
                                        Вам разве не важна скорость отображения вашего приложения, а только насколько быстро «а» изменится на «б» в рантайме? Мне как раз первое важнее — это и тестирую.

                                        Манипуляции — если это две разные модельные сущности в разных участках дома, то нет. Если же это списки то да, но лучше массивы менять через splice, чем 100 раз push — ну или просто сообщить маске, что нужно кешировать изменения — (lock model), а потом пакетом применить — (unlock model).
                                          0
                                          Окей, я вас понял, вы считаете, что старт ангулара за ~6ms может вызвать у пользователя дискомфорт? :]
                                            0
                                            На мобильном девайсе и со сложным UI и моделью будет далеко на 6мс… Как раз мобильной разработкой занимаемся — так там всё на счету. )
                                              0
                                              Окей, ~21ms :] И ведь это число будет почти константа.
                    0
                    О, как это мило, напоминает славные дни, когда я сутками сидел за собственным виью-ивент-фреймворком. Но к сожалению, пришли злые дяди и рассказали мне, что такое модели, зачем и главное как их надо использовать. Теперь я таким не страдаю.

                    Вообще очень советую вам ознакомиться близко с Backbone и его примерами (как я понял вы очень поверхностно его знаете). Только учтите, что если после прочтения вам хоть на секунду покажется, что он или ваш же фреймворк решает все проблемы, начните читать с начала. Потом изучите еще парочку. В какой-то момент озарение должно придти. Искренне желаю вам удачи, так как вы в любом случае молодец.
                      0
                      Спасибо! Но с событиями вы, наверное, делали что-то не так. В маске вы нигде на события(сигналы) не подписываетесь — всё довольно строго распределено — из любого контроллера или из любого дом элемента можно узнать, кто может / обрабатывает сигнал. В бэкбоне вы же наверняка подписываетесь на события? events: { «click .my-button»: «x»} — это называется «слушать разметку», а в этом есть большой недостаток — если есть иерархия контроллеров и каждый имеет свою разметку — тогда из предков, ни в коем случае нельзя «слушать разметку» детей. До этого, было как бы, два решения — первое, ребенок посылает «custom event», и второе, ребенок вызывает функции отца. Второе отпадает — из-за непозволительно большой связности. А «custom event» это уже лучше но лишние телодвижение — ребенок слушает свою разметку и посылает событие. Я же предлагаю выводить «событие»(сигнал) непосредственно в представление.

                      Модель:
                      В Backbone это именно модель
                      — вы, думаю, понимаете, что любые данные — это модель. Эти данные / состояния можно обернуть или скрыть в классах / структурах. И эти «обертки» собственно и называются domain model. Маска может работать и с тем, и с другим.

                        0
                        View из Backbone я выпиливаю за ненадобностью. Не слушать «разметку детей» задача довольно простая и никак с архитектурой может быть не связана. Различное понимание моделей я готов обсудить приватно. Вот моя почта tenphi@gmail.com
                        Не хочу разводить холивары, ведь тут могут быть дети.
                      0
                      Статья интересная, спасибо автору, вот только есть два вопроса, а вам не кажется что это выглядит сложно?
                      input #device-type type=value > :dualbind value="age" {
                          :validate match="^[a-z]{2}-[\d]{4}$" message=" ... pattern: xx-1234"
                      }
                      
                      

                      — вот тут два элемента или один, а внутри блока, вот эти параметры, это глюк хабра или так оно и есть? :]
                        0
                        Извините, ещё раз — какие параметры? )

                        Можно записать и так, (с аттр. type опечатка вышла)
                        input #device-type type=text {
                             :dualbind value="age" { 
                                  :validate match="^[a-z]{2}-[\d]{4}$" message=" ... pattern: xx-1234"; 
                                  // ....
                             }
                        }
                        


                        Согласен, что запись дуалбиндеров не очень лаконичная, но тэг нужен, так как через него можно указывать много других параметров BindingProvider-a.
                          0
                          Ну я не знаю, сходу так:
                          input #device-type type=text value="{{age}}"
                          

                          `:validate match="^[a-z]{2}-[\d]{4}$" message="… pattern: xx-1234"; ` — эти параметры, многоточие смутило.
                            0
                            Ааа. Это просто сообщение — может быть любой текст. Ваш пример отличный ({{age}}). Просто, понимаете, если будет много других параметров — changeEvent, signal listener, геттеры и сеттеры и прочее, то инлайн запись не подойдёт, через тэг тогда нужно будет.
                            Но простые записи двухсвязных биндингов обязательно сделаю проще. Спасибо за замечание.
                              0
                              Я понимаю, удачи вам.

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

                      Самое читаемое