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 вставляется в карман
Покадрово:
Кадр 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 — там весь этот путь нарисован одним
большим полотном с примерами и ссылками на все главы.