Задача
В целом, использование шаблонов Angular пользователем может выглядеть следующим образом: у нас есть некий набор данных:
const data = {
project: 'MySuperProject',
userName: 'Roman',
role: 'admin',
projectLink: 'https://example.com/my-super-projectproject'
}
Нужно дать возможность настроить текст письма, который будет отправляться пользователю после редактирования проекта. С помощью шаблона Angular это может выглядеть так:
<body>
Добрый день! Проект {{project}} доступен по ссылке <a href="{{projectLink}}">3D проект вашего заказа</a>
<div *ngIf="role == 'admin'">
Для редактирования проекта пройдите по ссылке <a href="{{projectLink}}?mode=edit">Редактировать</a>
</div>
</body>
Библиотека ng-template
Эту задачу можно решить использованием компилятора Angular на клиентской (или даже серверной стороне), но это весьма трудоёмко и потребует притащить много мегабайт кода на клиент. Почему же компилятор Angular такой большой? Это связано с тем, что он поддерживает море разнообразного функционала для композиции компонентов и модулей, а также содержит собственный парсер HTML! Поэтому я решил написать минимальный преобразователь шаблонов Angular, который будет использовать встроенный в браузер парсер HTML. Это удалось сделать всего лишь в 200 с небольшим строчек кода за пару часов. Результатом я решил поделиться с общественностью на GitHub
Использовать библиотеку ng-template довольно просто:
Устанавливаем зависимость из npm
npm install --save @quanterion/ng-template
или через yarn
yarn add @quanterion/ng-template
И используем следующим образом:
import { compileTemplate, htmlToElement } from '@quanterion/ng-template';
async test() {
let data = { name: 'Roman' };
let element = htmlToElement(`<div>{{name}}</div>`);
await compileTemplate(element, data);
alert(element.outerHTML);
}
Поддерживаемый синтаксис
- Выражения {{expression}} с возможностью доступа к переменным и вызова функций
- Шаблоны ng-template
- Контейнеры ng-container
- Условия *ngIf + *ngIf as
- Циклы *ngFor
- Стили [style.xxx]=«value» и [style.xxx.px]=«value»
- Условные классы [class.xxx]=«value»
- Observables {{name$}} c автоматической подпиской на значение (как пайп async)
Подробнее смотрите в тестах ng-template.spec.ts
Использование Eval
Для вычисления выражений в шаблонах используется eval с преферансом и куртизанками. Дело в том, что в шаблонах Angular доступ к переменным используется без привычного для JavaScript префикса this. Поэтому требуется вызвать eval(), у которого в области видимости лежат все переменные из объекта с данными. Сгенерировать такой код для eval() у меня не получилось, т.к. код вида
const data = { a: 1, b: () => 4 };
const expression = 'a+b()';
eval('a =1; b = ??;' + expression);
не позволяет передать функции
Решение было найдено путем создания функции, у которой параметры имеют имена полей объекта с данными:
const data = { a: 1, b: () => 4 };
let entries = []
for (let property in data ) {
entries.push([property, data[property]])
}
const params = entries.map(e => e[0]);
const fun = new Function('code', ...params, `return eval(code)`);
const args = entries.map(e => e[1]);
const expression = 'a+b()';
const result = fun.call(undefined, expression , ...args);
P.S.: Я надеюсь в будущем, когда API нового компилятора Ivy стабилизируется, можно будет генерировать набор операторов для Ivy и создавать полноценные компоненты в динамике!
Ссылка на исходники
Комментарии (10)
alexs0ff
15.04.2019 09:35+1Ivy
Оффтоп и ИМХО, но у меня на него пока скепсис.
разработчики уже в который раз рендеринг меняют для angular? 3 или 4. Так по мне, пока лучше пускай переживет несколько версий, и только потом можно будет на него полагаться.
tuxi
15.04.2019 11:14Такие вещи уже 15 лет делаются на XSLT, там из коробки и инклюд шаблонов в шаблон есть, и возможность предварительной компиляции шаблонов есть, и одинаково легко как на серверной стороне делаются, так и на клиентской. Что то типа такого
<data> <project>MySuperProject</project> <userName>Roman</userName> <role>admin</role> <projectLink>https://example.com/my-super-projectproject</projectLink> </data> <xsl:template name="letter"> <body> Добрый день! Проект <xsl:value-of select="./project"/> доступен по ссылке <a href="{./projectLink}">3D проект вашего заказа</a> <xsl:if test="./role = 'admin'"> <div> Для редактирования проекта пройдите по ссылке <a href="{./projectLink}?mode=edit">Редактировать</a> </div> </xsl:if> </body> </xsl:template>
Само собой, есть богатый инструмент для создания выборок, агрегации, проверок условий и прочая прочаяx512 Автор
15.04.2019 11:21Классная вещь! а может она работать не только со данными — объектами, но и вызывать функции, втч асинхронные? К, примеру, мне нужно в отчёт картинку сгенерить или диаграмму и параметры этой картинки или диаграммы я только в шаблоне указываю, возможности предварительно нагенерировать все варианты картинок, сами понимаете, нет.
tuxi
15.04.2019 11:25Никто не мешает скрестить ее с js :) xslt выполнит свою часть преобразования данных в нужный формат, а JS обеспечит ее нужными данными
x512 Автор
15.04.2019 11:29В том, то и прикол, что данные нужны в момент преобразования, а не после. К примеру, условие нужно или нет отображать эту картинку в принципе может браться из настроек по HTTP запросу. Т.е. для гибкости в моём случае нужно, чтобы любая часть шаблона вычислялась произвольным выражением. В этом и плюс Ангуляра, что у него везде в условиях, циклах итп вставляется кусок JS. А что это за кусок — обращение к данным или сложные функции — по барабану!
alexs0ff
А почему не взять тот же mustache, конечно синтаксис далеко не ангуларовский, но он портирован на многие языки и фреймворки.
Ну уж если хочется «ангулараподобия», есть шаблонизатор Tangular.
И очень бы хотелось иметь страничку для тестирования шаблонизатора с двумя полями (данные и шаблон) и кнопкой генерации.
x512 Автор
Я, конечно, присматривался к различным шаблонизаторам, но ряд причин побудил написать своё
1) Хотелось именно Angular синтакс из-за перспективы делать в будущем компиляцию на Ivy, чтобы в случае такого перехода не сломалась обратная совместимость
2) Размер — ряд шаблонизаторов, которые я смотрел (Blaze, Handlebars) имели размер > 50Kb
3) Асинхронность — возможность вставлять в шаблон данные из RxJs Observable, например, грузить картинки по HTTP