Feast
-----
Экспериментальный шаблонизатор на основе xhtml-синтаксиса с конвертацией в vdom ([citojs](https://github.com/joelrich/citojs)).


### Установка и запуск локального сервера

 * `cd feast`
 * `npm i --save-dev feast feast-dev` — установка необходимых пакетов
 * `node ./node_modules/feast-dev/index.js --feast-demo` — запуск сервера: [http://localhost:2015/](http://localhost:2015/)
 * [Подробнее о fesat-dev](https://gitlab.corp.mail.ru/mail/feast-dev#feast-dev)


### Настройка IDE / Подсветка кода / Language Injection

 - [WebStorm](#WebStorm)

---


### Работа из консоли

 - `./node_modules/feast/bin/assist create-block --name={name} --path=path/to/feast/blocks/`


### ES2015+ (+ @decorators)

```js
import {configure, Block} from 'feast';
import UIIcon from '../path/to/Icon/Icon';

@configure({
	name: 'btn', // название блока
	events: {tap: 'handleTap'}, // обработчики события
	isolate: false, // разрешить наследование значений аттрибутов родительского блока
	isolateEvents: false, // разрешить всплытие событий
	blocks: {icon: UIIcon}, // используемые блоки
	defaults: {text: 'Wow!'}, // свойства по умолчанию
	template: `<button remit:click="tap"><b:icon/>{attrs.text}</button>`,
})
export default class extends Block {
	handleTap(evt) {
		// ...
	}
}

// Читый ES2015+ (без декоратора)
export default configure({
	// Опции
})(class extends Block {
	// Методы
});
```


### TypeScript + TSX

```js
import {configure, Block} from 'feast';
import UIIcon from '../path/to/Icon/Icon';

export interface ButtonProps {
	text: string;
}

@configure({
	name: 'btn', // название блока
	events: {tap: 'handleTap'}, // обработчики события
})
export class extends Block<ButtonProps> {
	template({text}) {
		return (
			<button remit-click="tap">
				<Icon/>
				{text}
			</button>
		);
	}

	handleTap(evt) {
		// ...
	}
}
```


---

### Анимация

 * [fn:transition](#fn:transition)
 * [fn:if animated](#fn:if-animated)


### Базовые теги

 * [fn:if](#fn:if)
 * [fn:choose](#fn:choose): [fn:when](#fn:when) и [fn:otherwise](#fn:otherwise)
 * [fn:for](#fn:for)
 * [fn:value](#fn:value) и короткий синтакиси `{...}`
 * [fn:attr](#fn:attr)
 * [fn:add-class](#fn:add-class)
 * [fn:match](#fn:match)
 * [fn:apply-match](#fn:apply-match)

---


### Базовые атрибуты

 * [id](#id)
 * [ref](#ref)
 * [on-event](#on-event)
 * [remit:event](#remit:event)
 * [use:mixin](#use:mixin)

---

### Модификаторы
 * [Значения и выражения](#mods)

 * События
   * [Список](#event-modifiers)
   * [Расширение](#custom-event-modifiers)

---


### BEM

 * [bem:mod](#bem:mod) — элемент и атрибут
 * [bem:elem](#bem:elem) — спец. атрибут


---


### CSS Modules

 * [feast-css](#feast-css)
 * [feast-css: expose](#feast-css-expose)

---


### Создание блока

##### button.html — шаблон
```html
<div>
	<bem:mod name="disabled" test="attrs.disabled" />

	<div bem:elem="text">
		{attrs.text}
	</div>
</div>
```

##### button.js — описание поведения блока
[Полное описание методов feast.Block](#feast.Block)

```js
import feast from 'feast';
import template from 'feast-tpl!./button.html';
import styleSheet from 'feast-css!./button.css';

export default feast.Block.extend({
	name: 'button', // уникальное название блока
	template,
	styleSheet,
	defaults: {
		text: null,
		disabled: false
	}
});
```

---

<a name="fn:if"></a>
#### fn:if
Условные оператор.

 - `test` — любое javascript выражение

```html
<fn:if test="attrs.value > 10">
	Поздравляем!
</fn:if>

<!-- ИЛИ -->

<div fn:if="attrs.value === 777">
	Wow!
</div>
```

---

<a name="fn:if-animated"></a>
#### fn:if animated
Анимированные `if`.

 - `test` — любое javascript выражение
 - `animated` или `animated="slide"`

```html
<fn:if test="attrs.value > 10" animated>
	<b>Поздравляем!</b>
</fn:if>

<!-- ↓ первый frame ↓ -->
<b class="fx-enter">Поздравляем!</b>

<!-- ↓ анимация ↓ -->
<b class="fx-enter-active fx-enter-to">Поздравляем!</b>
```


<a name="fx"></a>
<a name="fn:transition"></a>
#### fn:transition
Анимация вложенного контента:

 - `name` — название эффекта (optional)
 - `appear` — добавить аниация на изначальный рендер (optional)

<a name="fx-phase"></a>
##### Фазы

 - **Изначальный рендер** (appear)
   1. Первый frame: `fx[-name]-appear-enter`
   2. Анимация: `fx[-name]-appear-enter-active fx[-name]-appear-enter-to`

 - **Добавление элемента**
   1. Первый frame: `fx[-name]-enter`
   2. Анимация: `fx[-name]-enter-active fx[-name]-enter-to`

 - **Удаление элемента**
   1. Первый frame: `fx[-name]-leave`
   2. Анимация: `fx[-name]-leave-active fx[-name]-leave-to`


```html
<fn:transition name="slide" appear>
	<b>Wow!</b>
</fn:transition>

<!-- ↓ первый frame ↓ -->
<b class="fx-slide-appear-enter">Wow!</b>

<!-- ↓ анимация ↓ -->
<b class="fx-slide-appear-enter-active fx-slide-appear-enter-to">Wow!</b>
```

---

<a name="fn:choose"></a>
<a name="fn:when"></a>
<a name="fn:otherwise"></a>
#### fn:choose
Блок ветвления.

```html
<fn:choose>
	<fn:when test="attrs.userName">
		Привет, {attrs.userName}!
	</fn:when>
	
	<fn:otherwise>
		Авторизуйтесь!
	</fn:otherwise>
</fn:choose>
```

---

<a name="fn:for"></a>
#### fn:for
Перебор массива или объекта.
**ВАЖНО**: У всех рутовых элементов внутри `fn:for` должен быть задан уникальный `key`-атрибут

 - `data` — массив или объект (любое javascript выражение)
 - `as` — название переменной очередного элемент массива (опционально)
 - `key` — индекс или ключ  (опционально)
 - `filter` — функция фильтрации списка, на вход получает два элемента: `as` и `key` (опционально)
 - `cached` — vdom-кеширование по ключу (опционально)

```html
<ul>
	<fn:for data="attrs.items" as="item">
		<li key="{item.id}">
			<a href="{item.href}">{item.text}</a>
		</li>
	</fn:for>
</ul>
```

---

<a name="fn:value"></a>
#### fn:value
Вывести любое javascript выражение

 - `output` — режим вывода
   - `raw` — «как есть»
   - `text` — обычный текст (по умолчанию)

```html
<h1>
	Привет <fn:value>attrs.username</fn:value>!
</h1>
```

или короткий синтаксис


```html
<h1>Привет {attrs.username}!</h1>
```

Чтобы вывести смивол `{` и `}` используйте экранирование `\{`.


---

<a name="fn:attr"></a>
#### fn:attr
Заменить или установить атрибут родительского элементу

 - `name` — название
 - `value` — значение
 - `test` — любое javascript выражение (опционально)

```html
<a>
	<fn:attr name="href" value="#!{attrs.href}" test="!attrs.disabled"/>
	...
</a>
```

---

<a name="fn:add-class"></a>
#### fn:add-class
Добавить css-класс родительскому элементу

 - `name` — название класса (через пробел)
 - `test` — любое javascript выражение (опционально)

```html
<div>
	<fn:add-class name="selected" test="attrs.value === 'foo'"/>
	...
</div>
```

---

<a name="fn:match"></a>
<a name="fn:apply-match"></a>
#### fn:match и fn:apply-match
Определение и использование подшаблона.

##### `fn:match`
 - `name` — имя подшаблона
 - `args` — названия аргументов через запятую, которые будут переданны в `match` от `apply-match` (опционально)


##### `fn:apply-match`

 - `name` — имя вызываемого подшаблона
 - `args` — аргументы через запятую, которые нужно передать в `match` (опционально)


**notify.html**
```html
<div>
	<h1 bem:elem="title"><fn:apply-match name="title"/><h1>
	<div bem:elem="content">
		<fn:apply-match name="content"/>
	</div>
</div>
```

**Использование:**
```html
<b:notify>
	<fn:match name="title">Заголовок</fn:match>
	<fn:match name="content">Какое-то содержание</fn:match>
</b:notify>
```

---

<a name="id"></a>
#### id
Доступ к вложеному `feast.Block` по уникальному `id`

 
```js
var App = feast.Block.extend({
	name: 'app',
	template: '<div><b:nav id="nav"/></div>',
	didMount() {
		const nav = this.ids.nav;
	}
});
```

---

<a name="ref"></a>
#### ref
Доступ к вложеному `HTMLElement` по уникальному `ref`

 
```js
var Form = feast.Block.extend({
	name: 'form',
	template: '<form><button ref="send"/></form>',
	didMount() {
		const sendEl = this.refs.send;
	}
});
```

---


<a name="on-event"></a>
#### on-event
Подписаться на DOM-событие

```html
<div on-click="_this.handleClick(evt)"/>
```

---

<a name="remit:event"></a>
#### remit:event
Преобразование DOM-события в пользовательское


```js
var Btn = feast.Block.extend({
	name: 'btn',
	template: '<button remit:click="tap" event:details="attrs.details"/>',
	events: {
		'tap': 'handleTap'
	},
	handleTap(evt) {
		const details = evt.details; // если `event:details` не задан, то details будет ссылкой на `this`
	}
});
```

<a name="remit:event-bubbles"></a>
#### Всплытие remit:event

##### Правильный способ организации всплытия `remit`-событий
```js
var SubscribeForm = feast.Block.extend({
	name: 'subscribe-form',
	template: feast.parse('<button remit:click="subscribe" value="..."/>')
});

var App = feast.Block.extend({
	name: 'app',
	blocks: {'subscribe-form': SubscribeForm},
	template: feast.parse('<div><b:subscribe-form type="news" remit:subscribe="subscribe:news"/></div>'),
	events: {
		'subscribe:news': 'handleSubscribe'
	},
	handleSubscribe(evt) {
		const type = evt.type.split(':').pop();
	}
});
```


---

<a name="mods"></a>
### Модификаторы значения и выражения

Сигнатура
```js
function format(value, format) {
	// применяем модицикацию
	return newValue;
}
```

Пример
```js
import dateFormat from './data-format';

feast.Block.extend({
	mods: {
		trim: (val) => val.trim(),
		ucfirst: (val) => val.charAt(0).toUpperCase() + val.substr(1),
		'bem-prefix': (val, prefix) => prefix + val,
		dateFormat
	},

	template: `<div class="{attrs.activeClass | trim | bem-prefix:'b-'}">
		Hello, {attrs.name | ucfirst}!
		Сейчас: {Date.now() | dateFormat:"hh:mm"}
	</div>`
});
```

---

<a name="event-modifiers"></a>
### Модификаторы события
Это инструмент, позволяющий на этапе подписки на событие применить к нему разные методы или фильтрацию,
например по названию клавиши или её коду:

```html
<!-- Перехватываем `enter`, отменяем действие по умолчанию и останавливаем всплытие -->
<input remit:keydown.enter.prevent.stop="send"/>
```

 - Модификатор события
   - `prevent` — отменить действие по умолчанию
   - `stop` — остановить всплытие
 - Фильтрация по
   - Названию клавиши
     - `enter`
     - `esc`
     - `tab`
     - `left`
     - `right`
     - `up`
     - `down`
     - `spacebar`
     - `shift`
     - `ctrl`
     - `alt`
     - `XXXXX` — произвольный `keyCode`
   - Зажатой клавише
     - `alt-pressed`
     - `ctrl-pressed`
     - `shift-pressed`
     - `meta-pressed`
   - Кнопке мыши
     - `left-btn`
     - `right-btn`

<a name="custom-event-modifiers"></a>

#### Расширение

```js
// on-click.my-modifier="..."
feast.vdom.eventModifiers['my-modifier'] = function myModifier(evt) {
	if (expression(evt)) {
		// Отменяем исполнение слушателя события
		return false;
	}
};

// Добавляем имя клавиши
feast.vdom.eventModifiers.KEYS.q = 'Q'.charCodeAt(0); // да-да, сначало в нижнем, потом верхнем регистре
```

---

<a name="use:mixin"></a>
#### use:mixin
Использовать примеси на стадии компиляции.

```js
feast.tags.$mixins['form-element'] = function formElementMixin(node, attrs) {
	attrs.name = '{attrs.name}';
	attrs.type = '{attrs.type || "text"}';
	attrs.tabindex = '{attrs.index}';
	attrs.placeholder = '{attrs.placeholder}';
	attrs.required = '{attrs.required}';
	// и так далее
};

var Inp = feast.Block.extend({
	name: 'inp',
	template: '<input use:mixin="form-element"/>'
});
```

---

<a name="bem:mod"></a>
#### bem:mod

 * `name` — название модификатора
 * `value` — значение модикатора (опционально)
 * `test` — условие добавления модификатора (любое javascript выражение, опционально)

```html
<div>
	<bem:mod name="flat"/>
	<bem:mod name="size" value="{attrs.size}"/>
	<bem:mod name="expanded" test="attrs.expanded"/>
</div>

<!-- ИЛИ -->
<div bem:mod="size_{attrs.size}">
	<!-- увы, но так можно добавить только один модификатор -->
</div>
```

---

<a name="bem:elem"></a>
#### bem:elem
Спец. атрибут для BEM-именования элементов (только плоское именование, никаких элемент элемента).

```html
<div>
	<h2 bem:elem="title">...</h2>
	<div bem:elem="text">...</div>
</div>
```

---

<a name="css-modules"></a>
### CSS Modules
Для работы используйте `feast/src/require-css.js`

```js
require.config({
	// ...
	map: {
		'*': {
			'feast-css': '/node_modules/feast/src/require-css.js',
			// ...
		},
	},
});
```

---


<a name="feast-css"></a>
#### feast-css

```js
import css from "feast-css!./style.css";

console.log(css.className); // -jdy73jk
```


---


<a name="feast-css-expose"></a>
#### feast-css: expose
Обеспечивает доступ к классом из внешнего css

```css
/* expose {form, container as formContainer} from "../form/form.css" */

.button { color: red; }
.form .button { color: green; }
.formContainer .container { margin: 10px; }
```


---

<a name="feast.Block"></a>
### feast.Block

```js
// Описание блока
import feast from 'feast';

import template from 'feast-tpl!path/to/block-name/block-name.html';
import styleSheet from 'feast-css!path/to/block-name/block-name.css'; // только если нужна модульность для CSS (уникальные имена CSS-селекторов)

// Используемые блоки
import UIBtn from 'path/to/btn/btn';

export default feast.Block.extend({
	name: 'block-name', // Имя блока, оно же используется как CSS-класс
	
	template, // или `'<div/>'`
	styleSheet, // опционально, только если нужна модульность, css: `{'block-name': '{uniq_hash}'}

	// Изолировать аттрибуты
	//   `true` — по умолчанию
	//   `false` — наследовать аттрибуты родителя
	isolate: false,

	// Изолировать события испускаемые блоком рамками этого блока, по умолчанию `true`
	isolateEvents: false,

	// Список используемых блоков
	blocks: {
		btn: UIBtn
	},

	// Обработка изменения атрибутов
	attrChanged: {
		'attr-name': function (newValue, oldValue) {
			// Любое действие
		}
	},

	// Обработка событий
	events: {
		'click': 'handleClick' // название события => название метода
	},

	handleClick(/** Event */evt) {
		// Обработка события `click`
	},

	didMount() {
		// Блок добавлен в DOM
		// Подписываемся на DOM событие (unmount элемента такие события будут сняты автоматически)
		this.$on(document, 'click', 'handleOutsideClick');
	},

	didUnmount() {
		// Блок извлечен из DOM
	},
	
	handleOutsideClick(/** Event */evt) {
		// Проверяем, что клик сделан за пределами блока
		if (this.el.contains(evt.target)) {
		}
	}
});


// Использование
import UIBlockName from 'path/to/block-name/block-name';

const block = new UIBlockName({
	'attr-bool': false,
});

// Рендер блока
block.renderTo(document.body);

// Уничтожить блок
block.destroy(); // генерируем событие `destroy`, на которое можно подписаться через `on`
```


#### Методы
 - **get**(attrName:`string`):`*` — получить значение атрибута
 - **set**(attrName:`string`, value:`*`):`void 0` — изменить значение атрибута
 - **set**(attributes:`object`):`void 0` — изменить значения атрибутов
 - **is**(attrName:`string`):`boolean` — проверить значение атрибута на истинность
 - **invert**(attrName:`string`):`boolean` — инвертировать значение атрибута
 - **on**(name:`string`, fn:`function`) — подписаться на событие блока
 - **off**(name:`string`, fn:`function`) — отписаться от события блока
 - **broadcast**(name:`string`[, details:`*`, [, originalEvent:`Event`]) — распространить событие вверх по vdom-дереву (т.е. дереву блоков)
 - **$on**(target:`HTMLElement`, eventName:`string`, handle:`string|function`) — подписаться на DOM событие
 - **$off**(target:`HTMLElement`[, eventName:`string`[, handle:`string|function`]]) — отписаться от DOM события или событий
 - **one**(target:`HTMLElement`[, eventName:`string`[, handle:`string|function`]])
 - **setTemplateMatch**(name:`string`, match:`Function`) — установить функцию отвечающую за `apply-match`


---

<a name="WebStorm"></a>
### WebStorm / Language Injection

 0. [Официальная документация](https://www.jetbrains.com/help/webstorm/2016.2/language-injections.html)
 1. Editor > Language Injection > \*click\* [+] > XML Attributes Injection
   - **name**: `feast-keywords`
   - **Language > ID**: `JavaScript`
   - **XML Tag > Local name**: `if|for|when|var|add-class|attr|match|mod|apply-match`
   - **XML Attrbiute > Local name**: `test|data|as|key|args`

 2. Editor > Language Injection > \*click\* [+] > XML Attributes Injection
   - **name**: `feast-interpolation-in-html`
   - **Language > ID**: `JavaScript`
   - **XML Attrbiute > Local name**: `[a-zA-Z-]+`
   - **Advanced > Value pattern**: `\{(.*?)\}`

 3. Editor > Language Injection > \*click\* [+] > XML Attributes Injection
   - **name**: `feast-interpolation-in-feast`
   - **Language > ID**: `JavaScript`
   - **XML Tag > Local name**: `mod|add-class|attr`
   - **XML Tag > Local namespace**: `bem|fn`
   - **XML Attrbiute > Local name**: `name|value`
   - **Advanced > Value pattern**: `\{(.*?)\}`

 4. Editor > Language Injection > \*click\* [+] > XML Tag Injection
   - **name**: `feast-value`
   - **Language > ID**: `JavaScript`
   - **XML Tag > Local name**: `value`
