Angular: ng-content для ng-template

—

от автора

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.

Twig 3.x documentation

В чём проблема

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.


ссылка на оригинал статьи https://habr.com/ru/post/669656/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *