13 — Content projection (ng-content) на пальцах
Цель: понять <ng-content> — как контент, который ты кладёшь между тегами
компонента, попадает внутрь него; что узлы при этом переезжают, а не копируются;
и кому они «принадлежат». Точные file:line — в notes/v21/.
Проблема, которую решает projection
Ты хочешь переиспользуемую «рамку» (карточка, модалка, кнопка), но содержимое каждый раз своё. Не передавать же весь HTML строкой. Решение: компонент оставляет «дырку», а пользователь вставляет туда свой контент.
<!-- card.html — шаблон компонента: -->
<div class="card">
<div class="card-head">Заголовок</div>
<ng-content></ng-content> ← ДЫРКА: сюда упадёт чужой контент
</div>
<!-- использование снаружи: -->
<app-card>
<p>Любой мой контент!</p> ← это и есть «спроецированный контент»
</app-card>
Результат в DOM:
<app-card>
<div class="card">
<div class="card-head">Заголовок</div>
<p>Любой мой контент!</p> ← переехал в дырку
</div>
</app-card>
Аналогия: компонент — это фоторамка, а <ng-content> — стекло-окошко, куда
вставляют твоё фото. Рамка одна, фото каждый раз разное.
Главное: узлы ПЕРЕЕЗЖАЮТ, а не копируются
Это ключевой момент. <ng-content> не дублирует твой контент — он физически
перемещает уже существующие DOM-узлы в новое место.
Кадр 1. Angular видит контент между <app-card>...</app-card> — узлы созданы.
Кадр 2. projectionDef раскладывает их по слотам (по селекторам ng-content).
Кадр 3. projection переносит готовые DOM-узлы в позицию <ng-content>.
Это ОДНИ И ТЕ ЖЕ узлы — не клоны.
Практическое следствие: нельзя использовать один <ng-content> дважды — узел
один, он не может быть в двух местах. Для повторов нужен <ng-template> + *ngTemplateOutlet
(см. 10-vstroennye-direktivy).
Мульти-слот: раскладываем по местам через select
Часто нужно несколько дырок: заголовок отдельно, тело отдельно, футер отдельно.
Тогда у <ng-content> есть атрибут select с CSS-селектором:
<!-- card.html: -->
<header><ng-content select="[card-title]"></ng-content></header>
<main> <ng-content></ng-content></main> ← без select = «всё остальное»
<footer><ng-content select="app-actions"></ng-content></footer>
<!-- использование: -->
<app-card>
<h2 card-title>Привет</h2> → уедет в <header>
<p>Тело карточки</p> → уедет в <main> (дефолтный слот)
<app-actions>...</app-actions> → уедет в <footer>
</app-card>
Как это работает: на create-фазе projectionDef проходит все переданные узлы и для
каждого ищет первый подходящий select. Что не подошло ни одному именованному
слоту — падает в слот без select (дефолтный). Совпадение селектора считается так
же, как для директив (см. 07-selektory) — сравнением с атрибутами/тегом узла, а не
живым DOM-поиском.
ngProjectAs — «притворись другим селектором»
Иногда узел надо отправить в слот, под чей select он не подходит напрямую (например,
обернул в <ng-container>). Тогда ngProjectAs заставляет узел «выдать себя» за
нужный селектор:
<ng-container ngProjectAs="[card-title]">
<h2>Заголовок из контейнера</h2>
</ng-container>
<!-- хоть это <ng-container>, для слота он матчится как [card-title] -->
Кому принадлежит спроецированный контент
Тонкий, но важный момент. Контент написан у родителя, а показан у потомка. Кому он «принадлежит»?
Спроецированный контент принадлежит родителю (тому, кто его написал) — его declaration view. У потомка он только отображается.
Из этого вытекает всё «странное» поведение:
• Биндинги в контенте считаются в контексте РОДИТЕЛЯ:
<app-card>{{ parentField }}</app-card> — parentField берётся у родителя, не у card.
• DI: сервис, который инжектит спроецированный контент, ищется по дереву РОДИТЕЛЯ
(см. [[09-di-injectory]]), а не потомка.
• Lifecycle: change detection контента идёт вместе с родителем.
Поэтому у компонента-обёртки (card) есть отдельный хук ngAfterContentInit (см.
11-lifecycle) — «мой спроецированный контент готов и вставлен». А contentChild
(см. 12-queries) ищет именно в этом — в спроецированном контенте.
А что в новом коде?
<ng-content> — это по-прежнему актуальный, не deprecated механизм (в отличие от
*ngIf/*ngFor). Control flow (@if/@for) и projection — про разное:
@if/@for— управляют своим шаблоном;<ng-content>— принимает чужой контент снаружи.
Новое рядом: можно задать fallback — что показать, если в слот ничего не передали
(содержимое внутри тега <ng-content>...</ng-content>).
Карта
родитель пишет контент между <app-card> ... </app-card>
│ (узлы созданы в declaration view родителя)
▼
projectionDef (create): разложить узлы по слотам по select=""
▼
projection (<ng-content>): ПЕРЕНЕСТИ (не копировать) узлы в дырку
▼
DOM: узлы видны внутри потомка,
НО принадлежат родителю (биндинги/DI/CD — родительские)
▼
у потомка: ngAfterContentInit + contentChild видят этот контент
Итог
<ng-content>— «дырка» в шаблоне, куда переезжает контент, переданный между тегами.- Узлы перемещаются, а не копируются → один
<ng-content>нельзя задействовать дважды. select="..."раскладывает по слотам; безselect— дефолтный слот «всё остальное»;ngProjectAsпозволяет узлу притвориться нужным селектором.- Контент принадлежит родителю (биндинги/DI/CD — его), у потомка только
отображается → отсюда
ngAfterContentInitиcontentChild.
Дальше → 14-host-bindings: как директива управляет своим собственным тегом и «приклеивает» к нему другие директивы.