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.