EN RU
Форум

Методология

Технологии

Инструментарий

Библиотеки

Учебные материалы

API

Выбор движка, компиляция и применение шаблонов

Движок BEMHTML

var bemxjst = require('bem-xjst');
var bemhtml = bemxjst.bemhtml;

// Добавляем шаблон
var templates = bemhtml.compile(() => {
    block('quote')({ tag: 'q' });
});

// Добавляем данные
var bemjson = { block: 'quote', content: 'Пришел, увидел, отшаблонизировал.' };

// Применяем шаблоны
var html = templates.apply(bemjson);

В результате html будет содержать строку:

<q class="quote">Пришел, увидел, отшаблонизировал.</q>

Движок BEMTREE

var bemxjst = require('bem-xjst');
var bemtree = bemxjst.bemtree;

// Добавляем шаблон
var templates = bemtree.compile(() => {
    block('phone')({
        content: { mask: '8-800-×××-××-××', mandatory: true }
    });

    block('page')({
        content: [
            { block: 'header' },
            { block: 'body' },
            { block: 'footer' }
        ]
    });
});

// Добавляем данные
var bemjson = [ { block: 'phone' }, { block: 'page' } ];

// Применяем шаблоны
var result = templates.apply(bemjson);
// result будет содержать:
[
    {
        block: 'phone',
        content: {
            mask: '8-800-×××-××-××',
            mandatory: true
        }
    },
    {
        block: 'page',
        content: [
            { block: 'header' },
            { block: 'body' },
            { block: 'footer' }
        ]
    }
]

Добавление шаблонов

Чтобы добавить шаблоны к экземпляру templates воспользуйтесь методом compile.

var bemxjst = require('bem-xjst');

// Создаём экземпляр класса templates
var templates = bemxjst.bemhtml.compile(() => {
    block('header')({ tag: 'h1' });
});

// Добавляем данные
var bemjson = { block: 'header', content: 'Документация' };

var html = templates.apply(bemjson);
// html: '<h1 class="header">Документация</h1>'

// Добавляем шаблоны к уже созданному экземпляру класса templates
templates.compile(() => {
    block('header')({ tag: 'h2' });
});

html = templates.apply(bemjson);
// Теперь HTML-тег стал h2 вместо h1:
// html: '<h2 class="header">Документация</h2>'

Если вам нужно собрать все шаблоны в бандл, то эффективнее использовать метод generate.

Настройки

Разделители в именовании БЭМ-сущностей

var bemxjst = require('bem-xjst');
var templates = bemxjst.bemhtml.compile(() => {
    // В этом примере мы не добавляем пользовательских шаблонов.
    // Для рендеринга HTML будет использовано поведение шаблонизатора по умолчанию.
    }, {
        // Настройка БЭМ-нейминга
        naming: {
            elem: '__',
            mod: { name: '--', val: '_' }
        }
    });

var bemjson = {
    block: 'page',
    mods: { theme: 'gray' },
    content: {
        elem: 'head',
        elemMods: { type: 'short' }
    }
};

var html = templates.apply(bemjson);

В результате html будет содержать строку:

<div class="page page--theme_gray">
    <div class="page__head page__head--type_short"></div>
</div>

Подробнее читай в cоглашении по именованию.

Поддержка JS-экземпляров для элементов (bem-core v4+)

В библиотеке bem-core начиная с 4 версии появилась поддержка JS-экземпляров для элементов.

Это требует добавления класса i-bem для элементов, имеющих JS-параметры.

Добиться этого можно с помощью опции elemJsInstances:

var bemxjst = require('bem-xjst');
var templates = bemxjst.bemhtml.compile(() => {
    // В этом примере мы не добавляем пользовательских шаблонов.
    // Для рендеринга HTML будет использовано поведение шаблонизатора по умолчанию.
    }, {
        // Включаем поддержку JS-экземпляров для элементов
        elemJsInstances: true
    });

var bemjson = {
    block: 'b',
    elem: 'e',
    js: true
};

var html = templates.apply(bemjson);

В результате html будет содержать строку:

<div class="b__e i-bem" data-bem='{"b__e":{}}'></div>

Закрытие одиночных элементов

Если у вас нет необходимости генерировать корректный XHTML (закрывать одиночные элементы), можно немного сэкономить, отключив опцию xhtml:

var bemxjst = require('bem-xjst');
var templates = bemxjst.bemhtml.compile(() => {
    // В этом примере мы не добавляем пользовательских шаблонов.
    // Для рендеринга HTML будет использовано поведение шаблонизатора по умолчанию.
    }, {
        // Выключаем режим XHTML
        xhtml: false
    });

var bemjson = {
    tag: 'br'
};

var html = templates.apply(bemjson);

В результате html будет содержать строку:

<br>

Опциональные закрывающие теги

При помощи опции omitOptionalEndTags шаблонизатор не будет выводить опциональные закрывающие теги. По умолчанию эта опция выключена.

Список опциональных закрывающих тегов можно найти в спецификациях HTML4 и HTML5.

var bemxjst = require('bem-xjst');
var templates = bemxjst.bemhtml.compile(() => {
    // В этом примере мы не добавляем пользовательских шаблонов.
    // Для рендеринга HTML будет использовано поведение шаблонизатора по умолчанию.
    }, {
        // Отключаем вывод опциональных закрывающих тегов
        omitOptionalEndTags: true
    });

var bemjson = {
    tag: 'table',
    content: {
        tag: 'tr',
        content: [
            { tag: 'th', content: 'table header' },
            { tag: 'td', content: 'table cell' }
        ]
    }
};

var html = templates.apply(bemjson);

В результате html будет содержать строку:

<table><tr><th>table header<td>table cell</table>

Атрибуты без кавычек

Спецификация HTML позволяет опустить необязательные кавычки у атрибутов, которые не содержат пробелов и прочих специальных символов. Подробности читайте в спецификациях HTML4 и HTML5.

С помощью опции unquotedAttrs вы можете включить такое поведение в рендеринге BEMHTML.

var bemxjst = require('bem-xjst');
var templates = bemxjst.bemhtml.compile(() => {
    // В этом примере мы не добавляем пользовательских шаблонов.
    // Для рендеринга HTML будет использовано поведение шаблонизатора по умолчанию.
    }, {
        // Разрешаем пропускать кавычки в атрибутах если это возможно:
        unquotedAttrs: true
    });

var bemjson = { block: 'b', attrs: { name: 'test' } };

var html = templates.apply(bemjson);

В результате html будет содержать строку:

<div class=b name=test></div>

data-атрибуты с одиночными кавычками

По умолчанию значение data-атрибутов заключено в двойные кавычки. Например, для данных:

{
  block: 'b',
  attrs: {
    id: 'without-changes',
    'data-test': '{"reqid":"42"}'
  }
}

Результат шаблонизации по умолчанию будет:

<div class="b" id="without-changes" data-test="{&quot;reqid&quot;:&quot;42&quot;}"></div>

Это приводит к экранированию всех двойных кавычек внутри атрибута. Это может быть неудобно при шаблонизации JSON-данных в data-атрибут. Опция singleQuotesForDataAttrs указывает что для data-аттрибутов будет использоваться одиночная кавычка, а двойные не нужно экранировать.

var bemxjst = require('bem-xjst');
var templates = bemxjst.bemhtml.compile(() => {
    // В этом примере мы не добавляем пользовательских шаблонов.
        // Для рендеринга HTML будет использовано поведение шаблонизатора по умолчанию.
        }, {
        // использовать одиночные кавычки для data-атрибутов
        singleQuotesForDataAttrs: true
    });

var bemjson = { block: 'b', attrs: {
  id: 'without-changes',
  'data-test': '{"reqid":"42"}'
} };

var html = templates.apply(bemjson);

Результат шаблонизации с опцией будет:

<div class="b" id="without-changes" data-test='{"reqid":"42"}'></div>

Экранирование

Вы можете включить экранирование содержимого поля content опцией escapeContent. В этом случае ко всем строковым значениям content будет применена функция xmlEscape.

var bemxjst = require('bem-xjst');
var templates = bemxjst.bemhtml.compile(() => {
    // В этом примере мы не добавляем пользовательских шаблонов.
    // Для рендеринга HTML будет использовано поведение шаблонизатора по умолчанию.
    }, {
        escapeContent: true
    });

var bemjson = {
    block: 'danger',
    // Потенциально опасный и неконтролируемый текст
    content: '&nbsp;<script src="alert()"></script>'
};

var html = templates.apply(bemjson);

В результате html будет содержать строку:

<div class="danger">&amp;nbsp;&lt;script src="alert()"&gt;&lt;/script&gt;</div>

Если вам нужно вывести строку без экранирования воспользуйтесь специальным значением поля content: { html: '…' }.

Пример

var bemxjst = require('bem-xjst');
var templates = bemxjst.bemhtml.compile(() => {
    // В этом примере мы не добавляем пользовательских шаблонов.
    // Для рендеринга HTML будет использовано поведение шаблонизатора по умолчанию.
    }, {
        escapeContent: true
    });

var bemjson = {
    block: 'trusted',
    // Безопасное содержимое
    content: 'I <3 you!'
};

var html = templates.apply(bemjson);

В результате html будет содержать строку:

<div class="trusted">I <3 you!</div>

Обратите внимание, что в content.html ожидается именно строка.

Runtime проверки ошибок в шаблонах и входных данных

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

var bemxjst = require('bem-xjst');
var bemhtml = bemxjst.bemhtml;

var templates = bemhtml.compile(() => {
  block('b')({ content: 'yay' });

  block('mods-changes')({
    def: (node, json) => {
      json.mods.one = 2;
      return applyNext();
    }
  });
}, { runtimeLint: true });

var html = templates.apply([
  { block: 'b' },

  // boolean attributes
  { block: 'b', attrs: { one: true, two: 'true' } },

  // mods for elem
  { block: 'c', elem: 'e', mods: { test: 'opa' } },

  // Присвоения в this.ctx.mods
  { block: 'mods-changes', mods: { one: '1', two: '2' } }
]);

В результате выполнения этого кода в STDERR будут записаны предупреждения:

BEM-XJST WARNING: boolean attribute value: true in BEMJSON: { block: 'b', attrs: { one: true, two: 'true' } }

Notice what bem-xjst behaviour changed: https://github.com/bem/bem-xjst/releases/tag/v4.3.3

BEM-XJST WARNING: mods for elem in BEMJSON: { block: 'c', elem: 'e', mods: { test: 'opa' } }

Notice what bem-xjst behaviour changed: https://github.com/bem/bem-xjst/releases/tag/v5.0.0

BEM-XJST WARNING: looks like someone changed ctx.mods in BEMJSON: { block: 'mods-changes', mods: { one: 2, two: '2' } } old value of ctx.mod.one was 1

Notice that you should change this.mods instead of this.ctx.mods in templates

Режим production

Вы можете использовать опцию production, чтобы отрендерить весь BEMJSON, даже если в одном из шаблонов произошла ошибка.

Пример

var template = bemxjst.compile(() => {
  block('b1')({
      attrs: () => {
        var attrs = applyNext();
        attrs.undef.foo = 'bar';
        return attrs;
      }
  });
}, { production: true });
var html = template.apply({ block: 'page', content: { block: 'b1' } });

html будет содержать <div class="page"></div>.

Если в результате выполнения шаблонов случится ошибка, то узел не будет отрендерен, но шаблонизатор продолжит работу, выведя в STDERR сообщение об этой ошибке.

$node index.js 1> stdout.txt 2> stderr.txt

$ cat stdout.txt
<div class="page"></div>

$ cat stderr.txt
BEMXJST ERROR: cannot render block b1, elem undefined, mods {}, elemMods {} [TypeError: Cannot read property 'undef' of undefined]

При использовании опции production со значением true вы можете определить свою собственную реализацию функции логирования ошибок взамен стандартному console.error. В этом случае в вашу кастомную функцию будут переданы два аргумента:

Определить свою реализацию onError можно любым способом расширив прототип BEMContext. Например так:

var bemxjst = require('bem-xjst');
var templates = bemxjst.bemhtml.compile('', { production: true });

templates.BEMContext.prototype.onError = function(context, err) { … };

Настройка exportName

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

За примером обратитесь к следующему разделу про создание бандла.

Подключение сторонних библиотек

Технологии BEMTREE и BEMHTML поддерживают возможность подключения сторонних библиотек как глобально, так и для разных модульных систем с помощью опции requires.

Для подключения укажите название библиотеки и в зависимости от используемой модульной системы:

{
    requires: {
        'lib-name': {
            globals: 'libName',           // Название переменной в глобальной видимости
            commonJS: 'path/to/lib-name', // Путь к модулю CommonJS относительно собираемого файла
            ym: 'lib-name'                // Имя модуля из YModules
        }
    }
}

В шаблонах модули будут доступны с помощью метода this.require, например:

block('button')({
    content() {
      var lib = this.require('lib-name');
      return lib.hello();
    }
});

Не обязательно указывать все модульные системы для подключения библиотеки.

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

{
    requires: {
        'lib-name': {
            globals: 'dependName' // Название переменной в глобальной видимости
        }
    }
}

В случае, если указано несколько вариантов подключения модуля, шаблон будет пытаться его получить в следующем порядке:

Таким образом, если модуль доступен в глобальной переменной, то внутрь шаблонов попадет значение переменной, даже если существует нужный модуль в CommonJS/YM.

И также, модуль из CommonJS приоритетнее модуля в YModules.

Пример подключения библиотеки moment

Указывается путь к модулю:

{
    requires: {
        moment: {
            commonJS: 'moment'  // Путь к модулю CommonJS относительно собираемого файла
        }
    }
}

В шаблонах модуль будет доступен с помощью метода this.require('moment'). Код шаблона пишется один раз, одинаково для исполнения в браузере и в Node.js:

block('post').elem('data')({
    content() {
        var moment = this.require('moment');  // Библиотека `moment`
        return moment(this.ctx.date) // Время в ms, полученное с сервера
            .format('YYYY-MM-DD HH:mm:ss');
    }
});

Расширение BEMContext

Вы можете расширять BEMContext, чтобы использовать в теле шаблона пользовательские функции.

var bemxjst = require('bem-xjst');
var templates = bemxjst.bemhtml.compile('');

// Расширяем прототип контекста
templates.BEMContext.prototype.hi = function(name) {
    return 'Hello, ' + name;
};

// Добавляем шаблоны
templates.compile(() => {
    block('b')({
        content: (node) => node.hi('templates')
    });
});

// Входные данные
var bemjson = { block: 'b' };

// Применяем шаблоны
var html = templates.apply(bemjson);

В результате html будет содержать строку:

<div class="b">Hello, templates</div>

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

Метод generate генерирует JS-код, который может быть передан и выполнен в браузере для получения объекта templates.

var bemxjst = require('bem-xjst');
var bemhtml = bemxjst.bemhtml;

var fs = require('fs');

var bundle = bemhtml.generate(() => {
    // пользовательские шаблоны
    // ...

}, { exportName: 'bemhtml8x' });

// Записываем бандл на файловую систему
fs.writeFile('bundle.js', bundle, (err) => {
  if (err) return console.error(err);
  console.log('Done');
});

В результате вы получите файл bundle.js, в котором будет лежать код движка и пользовательские шаблоны. Код движка будет доступен в переменной bemhtml8x и полностью готов к исполению в браузерах, node.js или любой виртуальной машине JS.

Source maps

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

Пример генерирования бандла с шаблонами и ядром bem-xjst с использованием карт кода:

var fs = require('fs'),
    bemxjst = require('bem-xjst').bemhtml,
    tmpl = 'my-block-1.bemhtml.js',
    bundle = 'bundle.bemhtml.js';

var result = bemxjst.generate(fs.readFileSync(tmpl, 'utf8'), {
    to: bundle,
    sourceMap: { from: tmpl }
});

fs.writeFileSync(bundle, result);

Так же смотрите примеры по использованию карт кода вместе с шаблонизатором.

Читать далее: входные данные