Angular: ng-content для ng-template
- понедельник, 6 июня 2022 г. в 00:38:00
Macros are comparable with functions in regular programming languages. They are useful to reuse template fragments to not repeat yourself.
Macros are defined in regular templates.
Content Projection - очень удобный инструмент организаци шаблонов. Тема неоднократно и хорошо разобрана на многочисленных ресурсах. Тем не менее, такой аспект как использование content projection совместно с ng-template удобной штатной реализации не имеет. С одной строны, это и не проблема совсем, поскольку сами компоненты c лихвой решают эту задачу. Но, если возникает необходимость и желание быть ближе к DRY без создания вспомогательных компонентов, то возможности, наподобие тех, что есть в Twig, Jinja2, Nunjucks и других шаблонизаторах весьма кстати.
<ng-template #tpl1 let-param let-mark="mark">
<div>
<ng-content></ng-content> <!-- Not works here -->
<ng-container *ngTemplateOutlet="param"></ng-container>
World {{mark}}
</div>
</ng-template>
<ng-template #paramTemplate1>
Hello
</ng-template>
<ng-template #paramTemplate2>
Hi
</ng-template>
<ng-container *ngTemplateOutlet="tpl1; context: {$implicit: paramTemplate1, mark: '!'}">
</ng-container>
<ng-container *ngTemplateOutlet="tpl1; context: {$implicit: paramTemplate2, mark: '!!!'}">
</ng-container>
Это пример решения задачи с использованием стандартных возможностей. У него очевидные проблемы, связанные и с читабельностью разметки и с её семантикой. Лично мне весьма сложно понять кто что рендерит и что в итоге получится.
Представляя себе конечный результат, ожидаешь увидеть что-то вроде:
<ng-template #tpl21 let-ctx>
<div>
<ng-macro-content></ng-macro-content>
World {{ctx.mark}}
</div>
</ng-template>
<ng-macro [template]="tpl21" [context]="{ mark: '!' }">
Hello
</ng-macro>
<ng-macro [template]="tpl21" [context]="{ mark: '!!!' }">
Hi
</ng-macro>
Для того, чтоб всё работало как ожидается, необходимо чтоб на уровне ng-macro
произошел захват ссылки на шаблон (TemplateRef
), и совместно с контекстом шаблона состояние было сохранено в дереве компонентов рендеринга (не совсем то же самое что и DI-иерархия). Соответственно, на уровне ng-macro-content
необходимо это состояние извлечь, и отредерить. Первая задача решается тривильно, а с решением второй приходится схитрить, и воспользоваться классом ViewContainerRef , который, благо, хранит нужное нам состояние в private поле _hostLView типа LView.
Нотация разметки в виде тегов хорошо читаема, но побочным продуктом такого подхода является появление соответствующих узлов в DOM документа.
Это неудобство можно устранить переписав решение в нотации атрибутов, т.е. через структурные директивы:
<ng-template #tpl22 let-ctx>
<div>
<span *ngMacroContent></span>
World {{ctx.mark}}
</div>
</ng-template>
<ng-container *ngMacro="tpl22; context: { mark: '!' }" >
Hello
</ng-container>
<span *ngMacro="tpl22; context: { mark: '!!!' }" >
Hi
</span>
На мой взгляд, такая нотация выглядит и компактней и читабельней, и в результате получится:
По причине использования закрытого API технически решение получилось не очень элегантное. По хорошему, такая возможность должа быть штатно (возможно, она и есть, но я про неё не знаю). Тем не менее, подход благополучно живёт в эксплуатации уже пару лет и не менее благополучно мигрирует со всеми обновлениями без потери работоспособности (иногда, с обновлениями фреймворка приходится вносить минимальные изменения). В целом, использование такого инструментария удобно. Фактически, ng-templatе
в рамках шаблона становятся полноценными функциями высшего порядка, со всеми вытекающими из этого обстоятельства возможностями, поскольку появляется удобный инструментарий их вызова и композиции.
Если у кого-то есть идеи как реализовать решение штатными средствами, то буду благодарен за совет.
Пример на StackBlitz для Angular 14.