No description, website, or topics provided.
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
docs Update 04-component-interaction.md Apr 16, 2017
images uploading logo Dec 18, 2015
.gitignore parse error fixed Oct 27, 2015
README.md Update README.md Apr 16, 2017
beast.js Update beast.js Apr 13, 2017

README.md

Содержание

Beast

Инструмент создания интерфейса веб-приложений. Покрывает собой весь цикл разработки: от шаблонизации до взаимодействия компонент. Идейный наследник БЭМ-методологии и инструментов i-bem.js, bh, React; результат бесед с Сергеем Бережным, Маратом Дулиным, Антоном Шеиным и Артемом Шитовым.

Основная идея: интерфейс делится на компоненты, каждый из которых характеризуется уникальным БЭМ-селектором, входными данными, правилами их преобразования в представление и описанием поведения.

В основе лежат три знания:

  • BML-разметка компонента (Beast markup language, XML-подмножество)
  • Методы компонента
  • Декларация развертки и поведения компонента

BML-РАЗМЕТКА

Любой интерфейс — это семантическая иерархия. Одни компоненты включают в себя другие, некоторые зависят от контекста, некоторым всё равно. Для независимых компонент введен термин блок. Блоки могут иметь вспомогательные компоненты — элементы. И те, и другие могут обладать изменяющимися признаками — модификаторами (состояния, типы поведения, темы внешнего вида).

Классический способ описания иерархий — XML. Его удобство в том, что он позволяет наглядно разделять содержимое сущности от ее признаков. Содержимое может быть текстом, другими сущностями или всем вперемешку. Пример иерархии интерфейса браузера:

<Browser>
    <head>
        <Tabs>
            <tab State="active">Yandex</tab>
            <tab State="release">Lenta.ru</tab>
            <newtab/>
        </Tabs>
        <Addressbar>https://yandex.ru</Addressbar>
    </head>
    <content>
        <Webview State="active" url="https://yandex.ru"/>
        <Webview State="release" url="http://lenta.ru"/>
    </content>
</Browser>

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

  • Browser
  • Tabs
  • Addressbar
  • Webview

И это не единственно верное разбиение интерфейса на главные и второстепенные компоненты. Можно было бы не выделять табы в отдельный блок, а сделать блоком каждый таб, и это была бы уже другая история:

<Browser>
    <head>
        <Tab State="active">Yandex</Tab>
        <Tab State="release">Lenta.ru</Tab>
        ...
    </head>
    ...
</Browser>

Так или иначе, названия независимых компонент (блоков) начинаются с заглавных, чтобы выделить начало нового смыслового куска. Названия подчиняемых элементов, не имеющих ценности в отсутствии своего родителя, начинаются со строчных.

Элементами, как правило, назначаются компоненты, которые вынуждены часто общаться с себеподобными и зависят от какого-то общего параметра (модификатора родительского блока, например). Им просто удобно быть рядом и образовывать изолированную систему частого обмена сообщениями. Границы такой группы обозначает родительский компонент — блок; он же хранит общие знаменатели группы.

В примере с разметкой браузера помимо узлов есть еще атрибуты. Они тоже бывают двух типов: модификаторы и параметры. Модификаторы, как упоминалось ранее, описывают ограниченное (предвариательно описанное) подмножество признаков компонента: особенности внешнего вида, состояния вкл/выкл и тому подобное. К параметрам относят чаще всего неотображаемые атрибуты компонента, но влияющие на его работу или внешний вид; ими могут быть: счетчики, идентификаторы, URL-адреса для отображения документов и тому подобное. Названия модификаторов начинаются с заглавной буквы, а параметров — со строчной.

Для отображения BML-дерева в браузере, строится соответствующее HTML-дерево:

<div class="browser">
    <div class="browser__head">
        <div class="tabs">
            <div class="tabs__tab tabs__tab_state_active">Yandex</div>
            <div class="tabs__tab tabs__tab_state_release">Lenta.ru</div>
            <div class="tabs__newtab"></div>
        </div>
        <div class="addressbar">https://yandex.ru</div>
    </div>
    <div class="browser__content">
        <div class="webview webview_state_active"></div>
        <div class="webview webview_state_release"></div>
    </div>
</div>

Несмотря на то, что HTML генерируется автоматически, логику генерации все равно следует понимать, чтобы писать CSS и селекторы для декларакций (о последних речь идет в следующей части). Итак, в HTML вся семантика из названий узлов ушла в их классы. Названия тоже частично поменялись: вместо Browser теперь div class="browser", а вместо tabs div class ="browser__tabs". Сделано это для того, чтобы каждый блок или элемент с модификатором или без можно было описать одним CSS-селектором:

.webview {
    position:absolute;
    top:0;
    bottom:0;
    left:0;
    right:0;
}
.webview_state_release {
    display:none;
}
.webview_state_active {
    display:block;
}

Такая нотация, во-первых, ускоряет время рендера стилей в браузере за счет сокращения количества селекторов для идентификации компонента и его модификации, а, во-вторых, страхует от персечений селекторов стилей при вложении одних компонент в другие. С использованием препроцессоров (например, Less) классы перестают пугать своей длиной:

.webview {
    position:absolute;
    top:0;
    bottom:0;
    left:0;
    right:0;
    &_state {
        &_release {
            display:none;
        }
        &_active {
            display:block;
        }
    }
}

BML-разметка предварительно компилируется в js-эквивалент (HTML-разметка генерируется после):

Beast.node(
    'Browser',
    {'context':this},
    Beast.node(
        'head',
        null,
        Beast.node(
            'Tabs',
            null,
            Beast.node('tab', {'State':'active'}, 'Yandex'),
            Beast.node('tab', {'State':'release'}, 'Lenta.ru'),
            Beast.node('newtab', null)
        ),
        Beast.node('Addressbar', null, 'https://yandex.ru')
    ),
    Beast.node(
        'content',
        null,
        Beast.node('Webview', {'State':'active', 'url':'https://yandex.ru'}),
        Beast.node('Webview', {'State':'release', 'url':'http://lenta.ru'})
    )
)

А раз конечный формат — javascript, ничто не мешает использовать его элементы в BML:

<Button id="{Math.random()}">Save</Button>

На самом же деле, парсер обрабатывает только подстроки вида <item>...</item>; и всё, что снаружи, остается всё тем же javascript. Поэтому можно писать так:

var label = "Save"
var node = <Button>{label}</Button>

В HTML-файл BML-разметка подключается двумя способами: либо тегом link с указанием type="bml" и адресом до файла, либо тегом script тоже с указанием type="bml", а внутри BML-дерево.

<html>
    <head>
        <script src="beast.js"></script>
        <link type="bml" href="browser.bml"/>
        <script type="bml">
            <Browser>...</Browser>
        </script>
    </head>
</html>

Для карткости теги html и head можно опускать — браузер сам сгенерирует эту структуру. И, если содержимое <script type="bml"> будет начинаться и заканчиваться BML-разметкой, парсер автоматически допишет финальный метод генерации и присоединения Beast.node(...).render(document.body) — так DOM-дерево станет дочерним элементом тега body. Рекомендуется также всегда указывать в начале кодировку utf-8. В итоге содержимое HTML-файла сократится до такого:

<meta charset="utf8">
<script src="beast.js"></script>
<link type="bml" href="browser.bml"/>

<script type="bml">
    <Browser>...</Browser>
</script>

Хорошей практикой считается подключение через link файлов с декларациями, а в script хранение общей разметки экрана или страницы.


Компонент (BemNode)

В javascript BML-разметка компилируется во вложенные друг в друга компоненты — экземпляры класса BemNode.

var button = <Button>Найти</Button>var button = Beast.node('Button', null, 'Найти')
↓
var button = new Beast.BemNode('Button', null, ['Найти'])

Методы BemNode могут модифицировать свой экземпляр: определять содержимое, поведение, параметры и так далее.

var button = <Button>Найти</Button>
var text = button.text()

button
    .tag('button')
    .empty()
    .append(<label tag="span">{text}</label>)
    .render(document.body)
    .on('click', function () {
        console.log('clicked')
    })

Работа с компонентами ведется внутри деклараций. Соответствие компонента и декларации устанавливается через селектор. Декларации исполняются в момент вызова метода render(). Метод render вызывается автоматичсеки при создании корневого компонента внутри тега <script type="bml">:

<meta charset="utf8">
<script type="text/javascript" src="beast.js"></script>
<link type="bml" href="browser.bml"/>

<script type="bml">
    <Browser>...</Browser>
</script>
↓
<script type="text/javascript">
    Beast.node('Browser', null, ...).render(document.body)
</script>

ДЕКЛАРАЦИЯ

Описывает развертку (expand) и поведение (domInit) компонента. Представляет собой метод Beast.decl() с двумя параметрами: селектор компонента (CSS-класс) и набор описаний.

Beast.decl('my-block', {
    expand: function () {},
    domInit: function () {}

})

Развертка

Входные данные блока не должны максимально подробно описывать результирующую структуру — следует указывать лишь изменяемые части. Например, блоке tabs:

<Tabs>
    <tab State="active">Yandex</tab>
    <tab State="release">Lenta.ru</tab>
    <newtab/>
</Tabs>

На самом же деле у каждого таба должен быть крестик, и раз его наличие обязательно, нет смысла это указывать каждый раз при разметке интерфейса. Тем не менее, чтобы крестик оказался в результирующем HTML-дереве, финальное BML-дерево должно быть соответствующим:

<Tabs>
    <tab State="active">
        <label>Yandex</label>
        <close/>
    </tab>
    <tab State="release">
        <label>Lenta.ru</label>
        <close/>
    </tab>
    <newtab/>
</Tabs>

Преобразование BML-дерева именуется разверткой. Правило развертывания описываются в декларации полем expand:

Beast.decl('tabs__tab', {
    expand: function () {
        this.append(
            <label>{this.text()}</label>,
            <close/>
        )
    }
})

Функция expand выполняется в контексте объекта BemNode. C каждым компонентом интерфейса связывается такой объект, речь о котором пойдет в следующем разделе; а пока будут упоминаться лишь некоторые из его методов.

append

В последнем примере используется метод append, разобраться в механизме и мотивах появления которого важно для понимания сути и удобства развертывания. Поскольку развертывание — это преобразование BML-дерева, должны быть состояния дерева до изменений и после. Можно было бы преобразовывать одно и то же дерево, тогда какой-нибудь более сложный код функции expand с несколькими вызовами append выглядел бы так:

<Article>
    <title>...</title>
    <author>...<author>
    <text>...</text>
<Article>
Beast.decl('article', {
    expand: function () {
        var title = this.get('title')
        var text = this.get('text')
        var author = this.get('author')

        this.empty()

        if (!this.mod('no-title')) {
            this.append(title)
        }

        this.append(text, author)
    }
})

Метод mod возвращает значение модификатора. В данном случае, если нет модификатора no-title, можно выводить заголовок. Метод empty очищает содержимое компонента перед началом присоединения дочерних элементов. Вне зависимости от порядка входных данных, имя автора должено стоять в конце статьи.

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

Beast.decl('article', {
    expand: function () {
        var content = []

        if (!this.mod('no-title')) {
            content.push(
                this.get('title')
            )
        }

        this.append(
            this.get('text'),
            this.get('author')
        )

        this.empty()
        this.append(content)
    }
})

Уже лучше. Теперь content играет роль временного массива дочерних элементов, который в конце работы метода заменяет старый. Но с таким подходом каждый метод expand будет традиционно начинаться с var content = [] и заканчиваться методами empty и append(content). От этой рутины можно избавиться, если метод append в контексте развертывания будет автоматически складывать свои аргументы в новый массив дочерних элементов, который после завершения метода expand заменит собой старый массив. Поэтому, с учетом вышесказанного, результирующий код выглядит так:

Beast.decl('article', {
    expand: function () {
        if (!this.mod('no-title')) {
            this.append(
                this.get('title')
            )
        }

        this.append(
            this.get('text'),
            this.get('author')
        )
    }
})
Поля mod и param

Также на этапе развертывания можно определить модификаторы и параметры компонента с их значениями по умолчанию. Такие определения полезны для автоспецификации API.

Beast.decl('tabs__tab', {
    mod: {
        state:'release'
    },
    param: {
        url:''
    },
    expand: function () {
    }
})
Направление обхода

Развертывание производится от родителя к ребенку. На примере с интерфейсом браузера:

<Browser>
    <head>
        <Tabs>...</Tabs>
        <Addressbar>...</Addressbar>
    </head>
    <content>
        <Webview/>
    </content>
</Browser>

Последовательность обхода: Browser, head, Tabs, Addressbar, content, Webview. Сделано это для того, чтобы всегда оставался шанс доуточнить содержимое и развернуть его тоже:

Beast
.decl('browser', {
    expand: function () {
        this.append(
            this.get('head'),
            this.get('content'),
            <statusbar/>
        )
    }
})
.decl('browser__statusbar', {
    expand: function () {
        console.log('statusbar expand')
    }
})

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

Поведение

После процедуры развертывания запускается процедура генерации DOM-дерева и его инициализации — чаще всего навешивание обработчиков событий.

Beast.decl('tabs__tab', {
    domInit: function () {
        this.on('click', function () {
            this.mod('state', 'active')
        })
    }
})

Методы обработки событий имеют свое поле в декларации:

Beast.decl('tabs__tab', {
    on: {
        click: function () {
            this.mod('state', 'active')
        }
    }
})

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

Наследование деклараций

Одни компоненты могут расширять или доуточнять другие. Например, общий принцип работы табов — переключение между друг другом — можно отделить от внешнего вида и прочих частностей в отдельный блок abstract-tabs. Так все последующие реализации табов смогут наследовать это поведение и дополнять его своим: например, табы браузера могут не только переключаться, но и закрываться

Beast

.decl('abstract-tabs__tab', {
    on: {
        click: function () {
            this.parentBlock().get('tab').forEach(function (tab) {
                tab.mod('state', 'release')
            })
            this.mod('state', 'active')
        }
    }
})

/* ... */

.decl('browser-tabs__tab', {
    inherits:'abstract-tabs__tab',
    expand: function () {
        this.append(this.text(), <close/>)
    },
    domInit: function () {
        this.get('close').on('click', function () {
            this.remove()
        }.bind(this))
    }
})

Пользовательские методы

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

.decl('browser-tabs__tab', {
    expand: function () {
        this.append(this.text(), <close/>)
    },
    domInit: function () {
        this.get('close').on('click', function () {
            this.close()
        }.bind(this))
    },
    close: function () {
        jQuery(this.domElem()).fadeOut(100, function () {
            this.remove()
        }.bind(this))
    }
})

Итого

  • Интерфейс представляется иерархией компонент двух типов: независимые блоки и зависимые элементы; иерархию описывает BML-разметка.
  • Разметка компилируется в цепочку вложенных методов Beast.node(), которая и формирует дерево компонентов.
  • Разметка должна иметь единственный корневой компонент.
  • Структура компонентов преобразуется и дополняется поведением через декларации.
  • Структуру компонентов повторяет соответствующее DOM-дерево, где каждый компонент получает DOM-узел.
  • DOM-узел хранит ссылку на свой компонент в свойстве bemNode.
  • Компоненты — это экземпляры класса BemNode.
  • Функции деклараций выполняются в контексте соответствущего компонента.
  • Модификация структуры компонентов производится только методами компонентов. На этапе инициализации и далее, когда за иерархией компонент закреплено DOM-дерево, изменения отражаются и на нем.

СПРАВОЧНИК

BemNode

isBlock ():boolean

Является ли компонент блоком.

isElem ():boolean

Является ли компонент элементом.

selector ():string

Получить селектор элемента block или block__elem.

parentBlock ([node:BemNode]) [:BemNode]

Получить или назначить родительский блок. Если компонент является блоком, он сам себе родительский блок. Если элемент имплементируется блоком (см. метод implementWith), parentBlock для блока вернет родительский блок элемента.

parentNode ([node:BemNode]) [:BemNode]

Получить или назначить родительский компонент.

domNode ():DOMElement

Получить соответствующий элемент DOM-дерева.

defineMod (defaults:object)

Объявить модификаторы и их значения по умолчанию.

this.defineMod({
    state:'release',
    type:'action',
    size:'M'
})

defineParam

Объявить параметры и их значения по умолчанию.

this.defineParam({
    url:'',
    maxlength:20
})

mod (modName:string, [modValue:string|boolean], [data:anything]) [:string|boolean]

Получить или назначить модификатор. При установке модификатора третьим аргументом можно передать параметр data — некая дополнительная информация обработчику. Например, при таком способе установки модификатора, не будет вызываться событие окна:

Beast.decl('popup', {
    onMod: {
        state: {
            active: function (isSilent) {
                if (!isSilent) this.triggerWin('active')
            }
        }
    },
    expand: function () {
        this.mod('state', 'active', true)
    }
})

То же самое, записанное иначе:

Beast.decl('popup', {
    expand: function () {
        this.onMod('state', 'active', function (isSilent) {
                if (!isSilent) this.triggerWin('active')
            })
            .mod('state', 'active', true)
    }
})

Несколько за один вызов:

.decl('tabs__tab', {
    expand: function () {
        this.mod({
                state:'release',
                theme:'normal'
            })
    }
})

togglemod (modName:string, modValue1:string|boolean, modValue2:string|boolean)

Переключить модификатор: если не установлен ни один, установить первый; если установлен первый, установить второй и наоборот.

param (paramName:string, [paramValue:anything]) [:anything]

Получить или назначить параметр.

Несколько за один вызов:

.decl('tabs__tab', {
    expand: function () {
        this.param({
                url:'#foo',
                num:1
            })
    }
})

domAttr (attrName:string, [attrValue:string]) [:string]

Получить или назначить аттрибут DOM-узла.

Несколько за один вызов:

.decl('tabs__tab', {
    expand: function () {
        this.domAttr({
                src: this.text(),
                width: 200
            })
    }
})

css (property:string, value:string|number) [:string]

Назначить или получить CSS-правило DOM-узла.

Назначить несколько за один вызов:

.decl('tabs__tab', {
    expand: function () {
        this.css({
                display:'block',
                cursor:'pointer'
            })
    }
})

state (stateName:string, [stateValue:string]) [:anything]

TODO: описать

on (eventName:string, handler:function)

Устанавливает обработчик DOM-события. Можно перечислять несколько событий через пробел. Метод trigger, как и метод mod, может инициировать событие вместе с дополнительной информацией — обработки события примет эту информацию первым аргументом.

Beast.decl('button', {
    domInit: function () {
        this.on('click', function () {})
            .on('mouseup mousedown', function (e, data) {})
    }
})

onWin ()

Реакция компонента на события окна, которое играет роль общей шины событий в том числе. Обработчик события выполняется в контексте текущего компонента.

Beast.decl('popup', {
    domInit: function () {
        this.onWin('resize', function () {
                this.updatePosition()
            })
            .onWin('popup:open', function (e) {
                if (!e.currentTarget.bemNode === this) {
                    this.hide()
                }
            })
    }
})

Названия событий общей шины от компонент автоматически предворяются селектором (родителького в случае с элементом) блока, чтобы исключить пересечение имен и сделать подписку более наглядной. В примере popup подписывается на событие открытия себеподобных и закрывается.

onMod (modName:string, modValue:string, handler:function)

Реакция компонента на изменение модификатора.

Beast.decl('popup', {
    expand: function () {
        this.onMod('state', 'active', function (isSilent) {
                if (!isSilent) this.triggerWin('active')
            })
            .mod('state', 'active', true)
    }
})

trigger (eventName:string, data:anything)

Инициирует DOM-событие.

triggerWin (eventName:string, data:anything)

Инициирует DOM-событие окна. К имени события окна автоматически добавляется префикс селектор:.

Beast.decl('popup', {
    domInit: function () {
        this.onWin('popup:open', function () {...})
            .triggerWin('open')
    }
})

index () :number

Порядковый номер компонента в массиве детей родительского узла. Отсчет с нуля.

empty ()

Удалить содержимое компонента.

remove ()

Удалить компонент.

append (child:BemNode|string...)

Добавить содержимое в конец списка детей. В контексте развертки дети добавляются во временный массив, в контексте поведения — в основной.

Beast.decl('button', {
    expand: function () {
        this.append('Найти')
            .append(
                <content>
                    <icon/>
                    <label>Найти</label>
                <content>
            )
            .append(
                'Найти',
                <label>Найти</label>,
                <content>
                    <icon/>
                    <label>Найти</label>
                <content>
            )
    },
    domInit: function () {
        this.append(
                'Найти',
                <label>Найти</label>,
                <content>
                    <icon/>
                    <label>Найти</label>
                <content>
            )
    }
})

appendTo (parentNode:BemNode)

Присоединить или переместить текущий компонент в конец списка детей другого компонента.

prependTo (child:BemNode|string...)

Добавить содержимое в начало списка детей.

prependTo (parentNode:BemNode)

Присоединить или переместить текущий компонент в начало списка детей другого компонента.

insertChild (child:BemNode|string|array, index)

Вставить компонент, строку или массив по индексу.

replaceWith (NewBemNode:BemNode)

Заменить текущую компоненту новым содержимым.

Beast.decl('tabs__tab', {
    expand: function () {
        this.replaceWith(
                <Tab>{this.text()}</Tab>
            )
    }
})

implementWith (NewBemNode:BemNode)

Расширение метода replaceWith — к новому компоненту добавляются CSS-классы и инициализация текущего.

Полезно для раскрытия элементов через блоки с наследованием поведения первых:

.decl('tab__close', {
    expand: function () {
        this.implementWith(
                <Button icon="/assets/icons/close.svg"/>
            )
    },
    on: {
        click: function () {
            console.log('Tab was closed')
        }
    }
})

/* ... */

.decl('button', {
    on: {
        click: function () {
            console.log('Button was clicked')
        }
    }
})

По нажатию на крестик таба в консоль выведется:

> Button was clicked
> Tab was closed

Имплементирующий компонент наследует не только поведение, но и пользовательские методы. Также имплементирующий откликается на имя имплементируемого в методе get(). Модификаторы тоже становятся общими.

text () :string

Получить конкатенацию дочерних текстовых элементов. Другими словами, просто текстовое содержимое компонента.

elem (name:string) :array

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

get (path:string...) :array|string

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

<Browser>
    <head>
        <Tabs>
            <tab State="active">Yandex</tab>
            <tab State="release">Lenta.ru</tab>
            <newtab/>
        </Tabs>
        <Addressbar>https://yandex.ru</Addressbar>
    </head>
    <content>
        <Webview State="active" url="https://yandex.ru"/>
        <Webview State="release" url="http://lenta.ru"/>
    </content>
</Browser>

Метод использует имена узлов, а не их селекторы по той причине, что ему приходится нырять в списки детей на разную глубину. Дети на тот момент еще не развернуты и не инициализированы; и, значит, пока не знают ничего о родительском узле, из контекста развертки которого к ним обращаются и который еще может поменяться; а также не имеют содержимого, обещанного соответствующей декларацией.

Метод одинаково работает как в контексте развертывания, так и в инициализации. Результаты работы метода не кешируются.

Beast.decl('browser', {
    expand: function () {
        this.get() // все дети включая текст
        this.get('/') // все дочерние компоненты
        this.get('../Player')[0] // соседний компонент с именем Player
        this.get('head')[0] // компонент шапки
        this.get('head')[0].sayTrue() // true
        this.get('head/Tabs/tab') // массив табов
        this.get('head/Tabs/') // все дочерные компоненты Tabs
        this.get('head/Tabs/tab', 'content/Webview') // массив табов и страниц
        this.get('head/Tabs/tab')[0].mod('state') // значение атрибута State первого таба
        this.get('head/Addressbar')[0].text() // текстовое содержимое адресной строки
    },
    domInit: function () {
        this.get('head')[0].sayTrue() // true
    }
})

Beast.decl('head', {
    sayTrue: function () {
        return true
    }
})

has (path:string...) :boolean

Вариация метода get(). Проверяет наличие дочерних компонент по селектору.

afterDomInit (callback:function)

Функция callback выполнится только после DOM-инициализации компонента. Полезно, когда элементу требуется взаимодействовать с инициализарованным родительским блоком (а элементы инициализируются вперед).

.decl('tabs', {
    onMod: {
        state: {
            uncommon: function () {...}
        }
    }
})
.decl('tabs__tab', {
    mod: {
        state:'release'
    },
    domInit: function () {
        var uncommonState = this.mod('state') !== 'release'

        this.parentBlock().afterDomInit(function () {
            if (uncommonState) {
                this.mod('state', 'uncommon')
            }
        })
    }
})

Обработчики onMod назначаются при создании DOM-узла компонента — на момент DOM-инициализации tabs__tab компонент tab еще не имел своего DOM-узла.

clone ()

Создает полную копию себя.

render (domNode:DOMElement)

По большей части служебный метод. Инициирует рекурсивный процесс развертки и инициализации поведения компонента. В аргументе указывается родительский DOM-элемент для корневого компонента. Например, так выглядит создание компонента и привязка его к DOM дереву с последующей инициализацией.

var button = <Button><label>Найти</label></Button>
button.render(document.body)

renderHTML () :string

Генерация текстового HTML компонента и его дочерних элементов.

inherited () :string

Вызов переопределенной функции из наследуемой декларации.

Beast.decl({
    foo: {
        on: {
            click: function () {
                console.log('foo click')
            }
        }
    },
    bar: {        
        inherits: 'foo',        
        on: {
            click: function () {
                this.inherited()
                console.log('bar click')
            }
        }
    }
})

// foo click
// bar click

isKindOf () :string

Проверка, соответствует ли компонент или его предки (см. поле декларации inherits) селектору.

expand (child:BemNode|string...)

Повторная перерисовка блока с новыми входными данными, с размерткой и инициализацией. Текущие дети блока удалятся, но модификаторы и параметры останутся.


Beast.decl (selector:string, rules:object)

Создание декларации для компонентов, соответствующих селектору. Поля декларации:

inherits

Наследование декларации. Пользовательские методы текущей декларации при совпадении имен с наследуемыми перекроют их.

Beast.decl('browser-tabs', {
    inherits: 'abstract-tabs' | ['abstract-tabs', 'common-tabs']
})

abstract

Объявляет декларацию абстрактной — это значит, что при наследовании ее селектор не попадет в классы DOM-узла. Такие декларации служат лишь для описания общего поведения.

Beast.decl('Locale', {
    abstract:true,
    string: function () {
        ...
    }
})

expand

Контекст развертки компонента.

Beast.decl('browser', {
    expand: function () {
        this.append(
                <head>{this.get('Tabs')}</head>,
                <content>{this.get('Webview')}</content>
            )
    }
})

Поля деклараций, описываемые ниже: mod, param, tag — компилируется в методы, исполняемые в этом самом контексте.

Beast.decl('browser', {
    mod: {
        state:'active'
    }
})
↓
Beast.decl('browser', {
    expand: function () {
        this.defineMod({state:'active'})
    }
})

mod

Декларативная форма метода BemNode::defineMod().

Beast.decl('button', {
    mod: {
        state:'release',
        type:'normal'
    }
})

param

Декларативная форма метода BemNode::defineParam().

Beast.decl('button', {
    param: {
        href:'',
        maxlength:20
    }
})

state

TODO: описать

tag

Декларативная форма метода BemNode::tag().

Beast.decl('link', {tag:'a'})

domAttr

Декларативная форма метода BemNode::domAttr().

noElems

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

<Button>
    <Link>
        <label>Найти</label>
    </Link>
<Button>
Beast.decl('link', {noElems:true})
<div class="button">
    <div class="link">
        <div class="button__label">Найти</div>
    </div>
</div>

domInit

Контекст инициализации поведения компонента.

Beast.decl('tabs__tab', {
    domInit: function () {
        this.get('close')
            .on('click', this.onCloseClick.bind(this))
    }
})

В этом же контексте выполняются скомпилированные в методы поля: on, onWin, onMod.

on

Декларативная форма метода BemNode::on().

Beast.decl('tabs__tab', {
    on: {
        click: function () {
            this.mod('state', 'active')
        },
        'mouseover mouseout': function () {
            this.playMouseReaction()
        }
    }
})

onWin

Декларативная форма метода BemNode::onWin().

Beast.decl('popup', {
    onWin: {
        resize: function () {
            this.updatePosition()
        },
        'popup:open': function (e) {
            if (!e.currentTarget.bemNode === this) {
                this.hide()
            }
        }
    }
})

onMod

Декларативная форма метода BemNode::onMod().

Beast.decl('tabs__tab', {
    onMod: {
        state: {
            active: function () {
                this.parentBlock().get('tab').forEach(function (tab) {
                    if (tab !== this) {
                        tab.mod('state', 'release')
                    }
                }.bind(this))
            }
        }
    }
})

Прочие методы Beast

node (name:string, attributes:object, child:string|BemNode...)

<Button Size="M" href="#foo">Перейти</Button>Beast.node('Button', {'Size':'M', 'href':'#foo'}, 'Перейти')

Служебный метод. Создает экземпляр класса BemNode. Эквивалент BML-разметки.

onReady (callback:function)

Выполнит callback-функцию, когда загрузятся все стили и скрипты.


Hello, world

Чтобы запустить элементарный проект, к html-странице нужно подключить файл библиотеки /src/beast-min.js, подключить js-декларации блоков, CSS-стили и составить семантическое дерево интерфейса:

<html>
    <head>
        <meta charset="utf8">

        <!-- Инструмент -->
        <script src="beast-min.js"></script>

        <!-- Декларации -->
        <link type="bml" href="browser.bml"/>

        <!-- Стили -->
        <link type="text/css" rel="stylesheet" href="browser.css">

        <!-- Дерево интерфейса -->
        <script type="bml">
            <Browser>
                <head>
                    <Tabs>
                        <tab State="active">Yandex</tab>
                        <tab State="release">Lenta.ru</tab>
                        <newtab/>
                    </Tabs>
                    <Addressbar>https://yandex.ru</Addressbar>
                </head>
                <content>
                    <Webview State="active" url="https://yandex.ru"/>
                    <Webview State="release" url="http://lenta.ru"/>
                </content>
            </Browser>
        </script>
    </head>
</html>

Теги html и head писать необязательно. И следует помнить об атрибуте type="bml" для тегов script и link; в первом случае он сообщит Beast, что нужна прекомпиляция кода, а во втором еще и подгрузка самого файла. Политика безопасности браузеров не позволяет получать доступ к содержимому, загруженному через <script src="..."> — поэтому используется универсальный тег <link>.

Дальнейшая работа

Практические советы, механизмы оптимальной организации сложных связей между компонентами, принятый codestyle, разработка проекта с чистого листа — всё это описывает учебник Beast Practice.

Welcome home, good hunter.