08 — Динамика и порталы

Цель: понять, как контент появляется, исчезает и «переезжает» на лету — от @if до модалок и тултипов. Точная версия — 04-portals-and-dynamic-views в соседнем vault'е.

Одна идея на всю главу

Любая «динамика» в Angular — это всегда три шага:

1. есть КАРМАН (LContainer) — место, куда вставлять
2. создаётся КОПИЯ view (LView) — из шаблона или из компонента
3. копия КЛАДЁТСЯ в карман, и её DOM вставляется на страницу

Всё остальное (@if, @for, ngTemplateOutlet, модалки, CDK Portal) — это разные обёртки над этими тремя шагами. Понял их — понял всё.

Кирпичик 1 — <ng-template> и TemplateRef

<ng-template> — это заготовка, которая сама по себе не рисуется. Это как рецепт в книге: пока не приготовишь — на тарелке пусто.

<ng-template #greeting>
  <p>Привет!</p>     <!-- этого НЕТ на экране, пока не "приготовим" -->
</ng-template>

Ссылка на эту заготовку называется TemplateRef. Главный метод — createEmbeddedView() = «приготовь по рецепту» = создай LView-копию из шаблона.

Кирпичик 2 — ViewContainerRef (карман с пультом)

ViewContainerRef — это «карман» (LContainer) плюс пульт управления им. Умеет:

  • createEmbeddedView(tpl) — приготовить заготовку и вставить;
  • createComponent(Comp) — создать компонент и вставить;
  • insert / move / remove — вставить готовую копию / переместить / убрать.

Аналогия: ViewContainerRef — это полка, на которую ты ставишь, переставляешь и снимаешь предметы. Сам предмет (view) делает TemplateRef или компонент, а полка отвечает за «куда поставить и в каком порядке».

Анимация: как view вставляется в карман

assets/view-insert.svg

Покадрово:

Кадр 1.  <ul> с пустым карманом:    <ul><!--anchor--></ul>
Кадр 2.  createEmbeddedView() готовит копию:   [<li>1</li>]  (ещё не в DOM)
Кадр 3.  insert → копия кладётся в карман, <li> вставляется перед anchor:
            <ul><li>1</li><!--anchor--></ul>
Кадр 4.  ещё раз для 2 и 3:
            <ul><li>1</li><li>2</li><li>3</li><!--anchor--></ul>

Именно так под капотом работает @for: на каждый элемент массива — одна копия view в кармане. Удалил элемент — копию вынули и уничтожили.

@if / @for — это обёртки над тем же

Ты не пишешь ViewContainerRef руками — за тебя это делают @if и @for (а раньше *ngIf/*ngFor). Внутри:

  • @if (cond) → когда cond стало true, createEmbeddedView тела; стало false — remove;
  • @for → на каждый элемент createEmbeddedView, при изменениях массива добавляет/убирает/переставляет копии.

Никакой особой магии — те же три шага из начала главы.

ngTemplateOutlet — «вставь вот этот рецепт сюда»

Если хочешь вставить заготовку в нужное место декларативно:

<ng-template #tpl><b>Содержимое</b></ng-template>
<div *ngTemplateOutlet="tpl"></div>   <!-- сюда вставится содержимое tpl -->

Под капотом — один вызов viewContainerRef.createEmbeddedView(tpl). Тонкая обёртка, ничего нового.

Создать компонент из кода

Иногда компонент нужно создать программно (динамические попапы, виджеты):

const ref = viewContainerRef.createComponent(MyDialog);  // создать и вставить
ref.instance.title = 'Привет';                            // настроить
ref.destroy();                                            // убрать

Или совсем без контейнера — функцией createComponent() из @angular/core в произвольный DOM-элемент. Те же три шага: создать LView компонента → вставить.

Порталы (CDK Portal) — «телепорт контента»

Теперь главное слово из задачи — порталы. Это инструмент из @angular/cdk (он не часть ядра, а отдельная библиотека) для одной задачи:

«Взять контент (шаблон, компонент или кусок DOM) и вставить его в совершенно другое место страницы.»

Зачем это надо: модалки, тултипы, выпадашки. Их визуально надо рисовать поверх всего (в конце <body>), а объявлять удобно внутри своего компонента. Портал и есть этот «телепорт».

Три вида порталов (что телепортируем)

  • ComponentPortal(Comp) — телепортируем компонент.
  • TemplatePortal(tpl) — телепортируем <ng-template>-заготовку.
  • DomPortal(el) — телепортируем уже существующий кусок DOM.

Куда телепортируем — PortalOutlet

  • CdkPortalOutlet — директива-приёмник [cdkPortalOutlet];
  • DomPortalOutlet — приёмник на произвольном DOM-элементе (например, в <body>).

А внутри — всё те же три шага!

Портал — это просто полиморфная обёртка: смотрит, что в него положили, и зовёт нужный знакомый метод:

attach(TemplatePortal)  → viewContainerRef.createEmbeddedView(tpl)   ← кирпичик 1+2
attach(ComponentPortal) → createComponent(Comp)                       ← компонент
attach(DomPortal)       → просто physически переносит DOM-узлы (appendChild)

То есть CDK Portal не добавляет нового механизма — он лишь даёт единый удобный интерфейс «прикрепи что угодно куда угодно» поверх createEmbeddedView / createComponent / переноса DOM. На этом построены CDK Overlay, Dialog, меню.

Карта всей динамики

                      ┌──────────────────────────────────────┐
ng-template ──→ TemplateRef.createEmbeddedView ─┐             │
component  ──→ createComponent ─────────────────┤             │
                                                ▼             │
                              ViewContainerRef.insert (КАРМАН) │
                                                ▼             │
                              копия LView в LContainer        │
                                  + DOM вставлен на страницу   │
                                                ▲             │
CDK Portal.attach() ────────────────────────────┘  (обёртка)  │
   TemplatePortal→createEmbeddedView                           │
   ComponentPortal→createComponent                             │
   DomPortal→перенос DOM                                       │
                      └──────────────────────────────────────┘

ViewContainerRef + TemplateRef — это движок. @if/@for, ngTemplateOutlet, CDK Portal — обёртки разной толщины над одним и тем же «положить копию view в карман».

Финал

Ты прошёл весь путь: код → компиляция → сборка → старт → работа → внутреннее устройство. Теперь открой prosto.canvas — там весь этот путь нарисован одним большим полотном с примерами и ссылками на все главы.