Введение в компоненты derby 0.6

imageПродолжаю серию (раз, два, три, четыре) постов по реактивному фуллстек javascript фреймворку derbyjs. На этот раз речь зайдет о компонентах (некий аналог деректив в ангуляре) — отличному способу иерархического построения интерфеса, и разбиения приложения на модули.Общая информация о компонентахКомпонентами в дерби 0.6 называются derby-шаблоны, вынесенные в отдельную область видимости. Давайте разбираться. Допустим у нас есть такой view-файл (я для демонстрации выбрал все тот же Todo-list — список дел из TodoMVC): index.html

Todos:

И Body: и new-todo: здесь шаблоны, как сделать new-todo компонентом? Для этого нужно в дерби-приложении его зарегистрировать:

app.component ('new-todo', function (){}); То-есть сопоставить шаблону некую функцию, которая будет отвечать за него. Проще некуда (хотя пример пока еще полностью бесполезен). Но что это за функция? Как известно функции в javascript могут задавать класс. Методы класса помещаются в прототип, это здесь и используется.Чуть развернем пример — привяжем input к реактивной переменной и создадим обработчик события on-submit. Сначала посмотрим как это было бы, если бы у нас не было компонент:

app.proto.addNewTodo = function (){ //… } Какие здесь недостатки:1. Засоряется глобальная область видимости (_page)2. Функция addNewTodo добавляется к app.proto — в большом приложении здесь будет лапша.Как будет если сделать new-todo компонентом:

app.component ('new-todo', NewTodo);

function NewTodo (){}

NewTodo.prototype.addNewTodo = function (todo){ // Обратите внимание модель здесь «scoped» // она не видит глобальных коллекций, только локальные var todo = this.model.get ('todo'); //… } Так, что поменялось? Во-первых внутри шаблона new-todo: теперь своя область видимости, здесь не видны _page и все другие глобальные коллекции. И, наоборот, путь todo здесь локальный, в глобальной области видимости он не доступен. Инакапсуляция — это здорово. Во-вторых функция-обработчик addNewTodo теперь тоже находится внутри класса NewTodo не засоряя app своими подробностями.Итак, derby-компоненты — это ui-элементы, предназначение которых в сокрытии внутренних подробностей работы определенного визуального блока. Здесь стоит отметить то, и это важно, что компоненты не предполагают загрузку данных. Данные должны быть загружены еще на уровне контроллера, обрабатывающего url.

Если компоненты предназначены для сокрытия внутренней кухни, какой же они имеют интерфейс? Как в них передаются параметры и получаются результаты?

Параметры передаются так же как и в обычный шаблон через атрибуты и в виде вложенного html-контента (об этом чуть позже). Результаты возвращаются при помощи событий.

Небольшая демонстрация на нашем примере. Передадим в наш компонент new-todo класс и placeholder для поля ввода, а введенное значение будем получать через событие:

index.html

Todos:

app.component ('new-todo', NewTodo); app.component ('todos-list:', TodosList);

function NewTodo (){}

NewTodo.prototype.addNewTodo = function (todo){ var todo = this.model.get ('todo'); // создаем событие, которое будет достуно снаружи // (в месте вызова компонента) this.emit ('newtodo', todo); }

function TodosList (){};

TodosList.prototype.add = function (todo){ // Вот так событие попало из одного компонента // в другой. Все правильно, именно компонент // отвечающий за список и будет заниматься // добавлением нового элемента }

Давайте все это обсудим и посмотрим, чего добились.Наш компонент new-todo теперь принимает 2 параметра: placeholder и inputClass и возвращает событие «addtodo», это событие мы перенаправляем компоненту todos-list, там его обрабатывает TodosList.prototype.add. Обратите внимание, создавая экземпляр компонента todos-list мы назначили ему алиас list, используя ключевое слово as. Именно поэтому в обработчике on-addtodo мы смогли прописать list.add ().

Таким образом new-todo полностью изолирован и никак не работает с внешней моделью, с другой стороны компонент todos-list полностью отвечает за список todos. Обязанности строго разделены.

Теперь стоит более подробно остановиться на параметрах, передаваемых компоненту.

Интерфейс компонент Необходимо отметить, что передача параметров в компоненты досталась им по наследству от шаблонов, поэтому большая часть функционал аналогична (если не сказано иное, примеры я буду приводить на шаблонах).Отметим, что шаблоны (как и компоненты) в html файлах derby подобны функциям, у них есть декларация, где описан сам шаблон. А так же есть (возможно многократный) вызов данного шаблона из других шаблонов.

# Синтаксис декларации шаблона (компонента) и что такое @content Атрибуты element, attributes и array являются необязательными. Что они обозначают? Рассмотрим на примерах: Атрибут element По умолчанию декларация и вызов шаблона выглядят как-то так:(Пока не обр)

  • {{@caption}}
  • Делать так не всегда удобно. Иногда хотелось бы вызвать шаблон не через тег view с соответствующим именем, а прозрачно, используя имя шаблона в качестве имени тега. Для этого и нужен атрибут element.

  • {{@caption}}
  • А можно даже так

    В таком варианте, мы не используем закрывающуюся часть тега, так как содержимое тега у нас отсутствует. А что это такое? Неявный параметр content При вызове шаблона мы используем тег view, либо тег именованный атрибутом element примерно так:

  • {{@caption}}
  • Оказывается, при вызове, между открывающейся и закрывающейся частью тега можно разместить какое-либо содержимое, например, текст или же какой-то вложенный html. Он будет передан внутрь шаблона неявным параметром @content. Давайте в нашем примере заменим caption, используя @content:

    Home Home Home

  • {{@content}}
  • Это очень удобно, позволяет скрывать подробности и значительно упрощать код верхнего уровня.

    Атрибуты attributes и arrays имеют к этому непосредственное отношение.

    Атрибут attributes Можно представить себе задачи, когда блок html-кода, передаваемого в шаблон, внутри шаблона не должен единым блоком быть вставлен в определенное место. Допустим, есть какой-то виджет, имеющий header, footer и основной контент. Вызов его мог бы быть каким-то таким:

    <-- содержимое -->
    <-- содержимое -->
    <-- содержимое --> А внутри шаблона widget будет какая-то сложная разметка, куда мы должны иметь возможность по отдельности вставить все эти 3 блока, в виде header, footer и bodyДля этого и нужен attributes:

    {{@header}} {{@body}} {{@footer}} Кстати, вместо body, вполне можно было бы использовать content, ведь все, что не перечислено в attributes (ну и, на самом деле, еще в arrays) попадает в content:

    Hello

    <-- содержимое -->
    <-- содержимое -->

    text

    {{@header}} {{@content}} {{@footer}} Здесь есть одно ограничение, все что мы перечислили в attributes должно встречаться во внутреннем блоке (вставляемом в шаблон) всего один раз. А что делать, если нам нужно больше. Если мы хотим, например, сделать свою реализацию выпадающего списка и элементов списка может быть много? Атрибут arrays Делаем свой выпадающий список, нам хочется, чтобы получившийся шаблон принимал аргументы примерно так:

    Разметка внутри dropdown будет довольно сложной, значит просто content нам не подойдет. Так же не подойдет attributes, потому что там есть ограничение — элемент option может быть только один. Для нашего случая идеальным будет использование аттрибута шаблона arrays: {{each @options}}

  • {{this.content}}
  • {{}} Как вы, наверное, заметили при декларации шаблона задается 'arrays=«option/options»' — здесь два имени:

    1. option — так будет называться html-элемент внутри dropdown-а при вызове2. options — так будет называться массив с элементами внутри шаблона, сами элементы внутри этого массива будут представлены объектами, где все атрибуты option-а станут полями объекта, а его внутренне содержимое, станет полем content.

    Программная часть компонент Как мы уже говорили, шаблон превращается в компонент, если для него зарегистрирована функция-конструктор.

    app.component ('new-todo', NewTodo);

    function NewTodo (){}

    NewTodo.prototype.addNewTodo = function (todo){

    var todo = this.model.get ('todo'); //… } У компонента есть предопределенные функции, которые будут вызваны в некоторые моменты жизни компонента — это create, init и destroy.

    # init Функция init вызывается как на клиенте, так и на сервере, до рендеринга компонента. Ее назначение в том, чтобы инициализировать внутреннюю модель компонента, задать значения по-умолчанию, создать необходимые ссылки (ref). // взято из https://github.com/codeparty/d-d3-barchart/blob/master/index.js function BarChart () {}

    BarChart.prototype.init = function () { var model = this.model; model.setNull («data», []); model.setNull («width», 200); model.setNull («height», 100);

    // … }; # create Вызывается только на клиенте после рендеринга компонента. Нужна для регистрации обработчиков событий, подключения к компоненту клиентских библиотек, подписок на изменение данных, запуска реактивных функций компонента и т.д. BarChart.prototype.create = function () { var model = this.model; var that = this;

    // changes in values inside the array model.on («all», «data**», function () { //console.log («event data:», arguments); that.transform () that.draw () }); that.draw (); }; # destroy Вызывается в момент уничтожения компонента, нужна для завершающих действий: освобождения памяти, отключения реактивных функций, отключения клиентских библиотек.

    Что доступно в this в обработчиках компонента? Во всех обработчиках компонента в this доступны: model, app, dom (кроме init), все алиасы к dom-элементам, и компонентам, созданным внутри компонента, parent-ссылка на компонент-родитель, ну и понятное дело все что мы сами поместили в prototype функции-конструктора компонента.

    Модель здесь с приведенной областью видимости. То-есть через this.model у компонента видна будет только модель самого компонента, если же вам необходимо обратиться к глобальной области видимости derby, используйте this.model.root, либо this.app.model.

    C app все понятно, это экземпляр derby-приложения, через него много что можно сделать, например:

    MyComponent.prototype.back = function (){ this.app.history.back (); } Через dom можно навешивать обработчики на DOM-события (доступны функции on, once, removeListener), например:

    // взято https://github.com/codeparty/d-bootstrap/blob/master/dropdown/index.js Dropdown.prototype.create = function (model, dom) { // Close on click outside of the dropdown var dropdown = this; dom.on ('click', function (e) { if (dropdown.toggleButton.contains (e.target)) return; if (dropdown.menu.contains (e.target)) return; model.set ('open', false); }); }; Чтобы полностью понять этот пример, нужно иметь ввиду, что this.toggleButton и this.menu — это алиасы для DOM-элементов, заданные в шаблоне через as:

    Посмотрите здесь: github.com/codeparty/d-bootstrap/blob/master/dropdown/index.html#L4-L11

    Все функции dom: on, once, removeListeners могут принимать четыре параметра: type, [target], listener, [useCapture]. Target — элемент, на который навешивается (с которого снимается) обработчик, если target не указан, он равен document. Остальные 3 параметра аналогичны соответствующим параметрам обычной addEventListener (type, listener[, useCapture])

    Алиасы на dom-элементы внутри шаблона задаются при помощи ключевого словa as:

    MainMenu.prototype.hide = function (){ // Например так $(this.menu).hide (); } Вынос компонент из приложения в отдельный модуль До этого мы рассматривали только компоненты, шаблоны которых уже были внутри каких-либо html-файлов приложения. Если же нужно (а обычно нужно) полностью отделить компонент от приложения делается следующее:

    Для компонента создается отдельная папка, в нее кладутся js, html, сss файлы (с файлами стилей есть небольшая особенность), компонент регистрируется в приложении при помощи функции app.component в которую передается только один параметр — функция-конструктор. Как-то так:

    app.component (require ('…/components/dropdown'));

    Заметьте, раньше, когда шаблон компонента уже присутствовал в html-файлах приложения, регистрация была другой:

    app.component ('dropdown', Dropdown);

    Давайте рассмотрим какой-нибудь пример:

    tabs/index.js

    module.exports = Tabs; function Tabs () {} Tabs.prototype.view = __dirname;

    Tabs.prototype.init = function (model) { model.setNull ('selectedIndex', 0); };

    Tabs.prototype.select = function (index) { this.model.set ('selectedIndex', index); }; tabs/index.html

    {{each @panes as #pane, #i}}
    {{#pane.content}}
    {{/each}}
    Стоит особое внимание обратить на строку:

    Tabs.prototype.view = __dirname; Отсюда derby возьмет имя компонента (оно же отсутствует в самом шаблоне, так как там используется 'index:'). Алгоритм простой — берется последний сегмент пути. Допустим _dirname у нас сейчас равен '/home/zag2art/work/project/src/components/tabs', это значит что в других шаблонах к данному компоненту можно будет обратиться через 'tabs', например так: Stuff’n More stuff Само же подключение данного компонента к приложению будет таким: app.component (require ('…/components/tabs')); Очень удобно оформлять компоненты в виде отельных модулей npm, например, www.npmjs.org/package/d-d3-barchart

    © Habrahabr.ru