Динамические view и порталы: TemplateRef / ViewContainerRef + CDK Portal
Версия 21.0.3, Ivy. Тема: как Angular создаёт и переносит контент динамически —
низкоуровневые примитивы ядра (ng-template, TemplateRef, ViewContainerRef,
createComponent) и тонкая обёртка над ними — CDK Portal. Структуры
(LContainer/LView/TNode) — 02-view-data-structures.
Важно: пакет
@angular/cdkНЕ входит вsrc-v21(там толькоcore/common). Часть про CDK Portal ниже описана по стабильному публичному API CDK (не менялось годами), но без построчной привязки к локальным исходникам — в отличие от части про ядро, которая выверена поsrc-v21.
Главная идея: всё сводится к LContainer + вставке LView
Любая «динамика» — это:
- где-то есть
LContainer(якорь, обычно comment-нода) — точка вставки; - создаётся
LView(из шаблона embedded-view или из компонента); - этот
LViewсплайсится в массивLContainerи его DOM-узлы физически вставляются в дерево.
TemplateRef/ViewContainerRef/ComponentRef/CDK Portal — разные «фасады» над этими
тремя шагами. Понятна механика — понятно всё остальное.
<ng-template> → TemplateRef
Компилятор для <ng-template> эмитит инструкцию ɵɵtemplate
(render3/instructions/template.ts:266). Она:
- создаёт
TNodeтипаContainer+ отдельныйTView(TViewType.Embedded) под содержимое шаблона; - заводит
LContainerв declaration-LView (шаблон САМ по себе ничего не рендерит — только «заготовка»).
TemplateRef (packages/core/src/linker/template_ref.ts:38) — это handle на такой
узел. Главный метод:
createEmbeddedViewImpl(context, injector?, dehydratedView?): EmbeddedViewRef<C> {
const embeddedLView = createAndRenderEmbeddedLView(
this._declarationLView, this._declarationTContainer, context, {...});
return new ViewRef(embeddedLView);
}
То есть createEmbeddedView клонирует TView шаблона в новый LView, прогоняет
create-проход и отдаёт EmbeddedViewRef. Но сам по себе он ещё не вставлен в DOM —
вставкой управляет ViewContainerRef (либо это делает структурная директива).
context становится LView[CONTEXT] — отсюда $implicit, let-x в шаблоне.
ViewContainerRef — императивная вставка
packages/core/src/linker/view_container_ref.ts:129. Обёртка над конкретным
LContainer, заякоренным на comment-узле. API: createEmbeddedView, createComponent,
insert, move, remove, detach, indexOf, get, length.
createEmbeddedView
view_container_ref.ts:386 — делегирует в TemplateRef, затем вставляет:
const viewRef = templateRef.createEmbeddedViewImpl(context || {}, injector, dehydratedView);
this.insertImpl(viewRef, index, shouldAddViewToDom(...));
return viewRef;
createComponent (v15+ API)
view_container_ref.ts:429 — принимает тип компонента (не фабрику). Внутри строит
ComponentRef через ComponentFactory.create и вставляет его host-view:
const componentDef = getComponentDef(componentType)!;
const componentRef = componentFactory.create(contextInjector, projectableNodes, rNode,
environmentInjector, directives, bindings);
this.insertImpl(componentRef.hostView, index, shouldAddViewToDom(...));
return componentRef;
Единая точка вставки: insertImpl
view_container_ref.ts:590 — куда сходятся ОБА пути (embedded и component):
private insertImpl(viewRef, index?, addToDOM?): ViewRef {
const lView = viewRef._lView!;
if (viewAttachedToContainer(lView)) { /* уже где-то вставлен → сначала detach */ }
const adjustedIdx = this._adjustIndex(index);
addLViewToLContainer(this._lContainer, lView, adjustedIdx, addToDOM); // <-- сплайс + DOM
viewRef.attachToViewContainerRef();
addToArray(getOrCreateViewRefs(lContainer), adjustedIdx, viewRef);
return viewRef;
}
addLViewToLContainer (render3/view/container.ts:98):
insertView(tView, lView, lContainer, index); // логически: в массив LContainer + view tree
if (addToDOM) {
const beforeNode = getBeforeNodeForView(index, lContainer);
const parentRNode = renderer.parentNode(lContainer[NATIVE]);
if (parentRNode !== null)
addViewToDOM(tView, lContainer[T_HOST], renderer, lView, parentRNode, beforeNode);
}
addViewToDOM → applyView(... WalkTNodeTreeAction.Insert ...) обходит TNode-дерево
view и зовёт nativeInsertBefore/nativeAppendChild
(render3/dom_node_manipulation.ts:47) — это и есть физическая вставка в DOM перед
якорной нодой. move = insert уже присоединённого view (view_container_ref.ts:641);
remove/detach делают обратное (detachView + destroyLView).
Структурные директивы (@if/@for, *ngIf) под капотом используют ровно это:
инжектят ViewContainerRef + TemplateRef и зовут createEmbeddedView/remove. В v21
@if/@for компилируются в ɵɵconditional/ɵɵrepeater, но механика контейнера та же
(см. 03-runtime).
ngTemplateOutlet
packages/common/src/directives/ng_template_outlet.ts:44 — тончайшая обёртка: на
изменение входа удаляет старый view и зовёт
viewContainerRef.createEmbeddedView(templateRef, ctx, {injector}). Никакой магии —
просто публичный API контейнера.
createComponent — компонент без контейнера
packages/core/src/render3/component.ts:84 — standalone-функция createComponent из
@angular/core для императивного создания компонента в произвольный host-элемент:
createComponent(component, { environmentInjector, hostElement?, projectableNodes?, ... })
→ new ComponentFactory(getComponentDef(component)).create(injector, projectableNodes,
hostElement, environmentInjector, directives, bindings);
ComponentFactory.create (render3/component_ref.ts:254) создаёт root TView/LView,
находит/создаёт host-элемент (locateHostElement, если задан селектор/узел — см.
03-selector-matching), гоняет directiveHostFirstCreatePass →
createDirectivesInstances → renderView, возвращает ComponentRef. Это же — путь
bootstrap корня. Чтобы такой компонент попал в CD-дерево, его host-view либо вставляют в
ViewContainerRef, либо регистрируют через ApplicationRef.attachView.
EmbeddedViewRef / ViewRef
render3/view_ref.ts:51 — публичная обёртка над LView:
rootNodes—collectNativeNodes(...)собирает верхние DOM-узлы view;context—lView[CONTEXT];destroy()— отцепляет изLContainer[VIEW_REFS],detachView,destroyLView;- реализует
ChangeDetectorRef(detectChanges,markForCheck) для этого view.
CDK Portal — полиморфная обёртка (не в src-v21)
@angular/cdk/portal. Идея: «контент (шаблон ИЛИ компонент ИЛИ кусок DOM), который можно
динамически воткнуть в любой PortalOutlet». Это фасад над теми же createEmbeddedView /
createComponent / перенос DOM — ничего нового на уровне рантайма не добавляет.
Портал (что переносим):
TemplatePortal(templateRef, viewContainerRef, context?)— обёртка надTemplateRef.ComponentPortal(component, viewContainerRef?, injector?)— обёртка над типом компонента.DomPortal(element)— уже существующий кусок DOM.
Outlet (куда вставляем): PortalOutlet/BasePortalOutlet; две реализации:
DomPortalOutlet— императивный, привязан к произвольному DOM-элементу (нужныComponentFactoryResolver/ApplicationRef/Injector);CdkPortalOutlet— директива[cdkPortalOutlet], держитViewContainerRef.
Диспетчеризация attach(portal) по типу:
attach(TemplatePortal) → viewContainerRef.createEmbeddedView(templateRef, context)
attach(ComponentPortal) → viewContainerRef.createComponent(component, ...) // или factory.create в DomPortalOutlet
attach(DomPortal) → физически переносит portal.element в host outlet'а (appendChild)
То есть TemplatePortal = createEmbeddedView (наш путь выше), ComponentPortal =
createComponent, DomPortal = голый перенос DOM-узлов. CDK добавляет лишь единый
полиморфный интерфейс + хуки attached/detach. На этом построены CDK Overlay, Dialog,
меню и т.п.
Сквозная линия
ng-template ─ɵɵtemplate→ LContainer + TemplateRef
│
TemplateRef.createEmbeddedView ─┐ │ ComponentFactory.create ─┐
▼ ▼ ▼
ViewContainerRef.insertImpl (root/host LView)
│
addLViewToLContainer → insertView (массив LContainer + view tree)
→ addViewToDOM → applyView → nativeInsertBefore
▲
CDK Portal.attach() ───────┘ (TemplatePortal→createEmbeddedView,
ComponentPortal→createComponent, DomPortal→DOM move)
ViewContainerRef / TemplateRef — это движок; ngTemplateOutlet, @if/@for, CDK
Portal — фасады разной толщины над одной и той же вставкой LView в LContainer.
Привязка к исходникам (ядро)
render3/instructions/template.ts:266—ɵɵtemplate(ng-template → LContainer+TView).linker/template_ref.ts:38—TemplateRef.createEmbeddedViewImpl.linker/view_container_ref.ts:129—ViewContainerRef;:386—createEmbeddedView;:429—createComponent;:590—insertImpl(единая точка вставки);:641—move.render3/view/container.ts:98—addLViewToLContainer;:194—insertView.render3/node_manipulation.ts:213—addViewToDOM;render3/dom_node_manipulation.ts:47—nativeInsertBefore/nativeAppendChild.render3/view_ref.ts:51—ViewRef/EmbeddedViewRef.render3/component.ts:84— standalonecreateComponent;render3/component_ref.ts:254—ComponentFactory.create.common/src/directives/ng_template_outlet.ts:44—ngTemplateOutlet.- CDK Portal:
@angular/cdk/portal(внеsrc-v21).