feast

Feast — Экспериментальный шаблонизатор на основе xhtml-синтаксиса с конвертацией в vdom

Usage no npm install needed!

<script type="module">
  import feast from 'https://cdn.skypack.dev/feast';
</script>

README

Feast

Экспериментальный шаблонизатор на основе xhtml-синтаксиса с конвертацией в vdom (citojs).

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

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


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

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

ES2015+ (+ @decorators)

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

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) {
        // ...
    }
}

Анимация

Базовые теги


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


Модификаторы


BEM

  • bem:mod — элемент и атрибут
  • bem:elem — спец. атрибут

CSS Modules


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

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

    <div bem:elem="text">
        {attrs.text}
    </div>
</div>
button.js — описание поведения блока

Полное описание методов feast.Block

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
    }
});

fn:if

Условные оператор.

  • test — любое javascript выражение
<fn:if test="attrs.value > 10">
    Поздравляем!
</fn:if>

<!-- ИЛИ -->

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

fn:if animated

Анимированные if.

  • test — любое javascript выражение
  • animated или animated="slide"
<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>

fn:transition

Анимация вложенного контента:

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

Фазы
  • Изначальный рендер (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
<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>

fn:choose

Блок ветвления.

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

fn:for

Перебор массива или объекта. ВАЖНО: У всех рутовых элементов внутри fn:for должен быть задан уникальный key-атрибут

  • data — массив или объект (любое javascript выражение)
  • as — название переменной очередного элемент массива (опционально)
  • key — индекс или ключ (опционально)
  • filter — функция фильтрации списка, на вход получает два элемента: as и key (опционально)
  • cached — vdom-кеширование по ключу (опционально)
<ul>
    <fn:for data="attrs.items" as="item">
        <li key="{item.id}">
            <a href="{item.href}">{item.text}</a>
        </li>
    </fn:for>
</ul>

fn:value

Вывести любое javascript выражение

  • output — режим вывода
    • raw — «как есть»
    • text — обычный текст (по умолчанию)
<h1>
    Привет <fn:value>attrs.username</fn:value>!
</h1>

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

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

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


fn:attr

Заменить или установить атрибут родительского элементу

  • name — название
  • value — значение
  • test — любое javascript выражение (опционально)
<a>
    <fn:attr name="href" value="#!{attrs.href}" test="!attrs.disabled"/>
    ...
</a>

fn:add-class

Добавить css-класс родительскому элементу

  • name — название класса (через пробел)
  • test — любое javascript выражение (опционально)
<div>
    <fn:add-class name="selected" test="attrs.value === 'foo'"/>
    ...
</div>

fn:match и fn:apply-match

Определение и использование подшаблона.

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

notify.html

<div>
    <h1 bem:elem="title"><fn:apply-match name="title"/><h1>
    <div bem:elem="content">
        <fn:apply-match name="content"/>
    </div>
</div>

Использование:

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

id

Доступ к вложеному feast.Block по уникальному id

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

ref

Доступ к вложеному HTMLElement по уникальному ref

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

on-event

Подписаться на DOM-событие

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

remit:event

Преобразование DOM-события в пользовательское

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`
    }
});

Всплытие remit:event

Правильный способ организации всплытия remit-событий
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();
    }
});

Модификаторы значения и выражения

Сигнатура

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

Пример

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>`
});

Модификаторы события

Это инструмент, позволяющий на этапе подписки на событие применить к нему разные методы или фильтрацию, например по названию клавиши или её коду:

<!-- Перехватываем `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

Расширение

// 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); // да-да, сначало в нижнем, потом верхнем регистре

use:mixin

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

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"/>'
});

bem:mod

  • name — название модификатора
  • value — значение модикатора (опционально)
  • test — условие добавления модификатора (любое javascript выражение, опционально)
<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>

bem:elem

Спец. атрибут для BEM-именования элементов (только плоское именование, никаких элемент элемента).

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

CSS Modules

Для работы используйте feast/src/require-css.js

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

feast-css

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

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

feast-css: expose

Обеспечивает доступ к классом из внешнего css

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

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

feast.Block

// Описание блока
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

WebStorm / Language Injection

  1. Официальная документация
  2. 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
  1. 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: \{(.*?)\}
  1. 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: \{(.*?)\}
  1. Editor > Language Injection > *click* [+] > XML Tag Injection
  • name: feast-value
  • Language > ID: JavaScript
  • XML Tag > Local name: value