javascript

Angular: ng-content для ng-template

  • понедельник, 6 июня 2022 г. в 00:38:00
https://habr.com/ru/post/669656/
  • Разработка веб-сайтов
  • JavaScript
  • Angular


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.