12 — Queries: как найти свой элемент на пальцах
Цель: понять @ViewChild / @ContentChild и новые сигнальные viewChild() /
contentChild() — как Angular находит элемент/компонент в шаблоне и когда этот
результат готов. Точные file:line — в notes/v21/.
Зачем это вообще
Иногда из кода компонента надо дотянуться до конкретного куска шаблона: получить
ссылку на <canvas>, вызвать метод дочернего компонента, измерить <div>. Руками
через document.querySelector — плохо (хрупко, ломает SSR, не знает про Angular).
Query — это «встроенный, правильный querySelector», который ищет внутри твоего
компонента и отдаёт нужный объект.
Что можно искать (предикат)
Query матчит узлы по одному из трёх признаков:
1. по template reference (#ref): viewChild('chart') ← ищем <canvas #chart>
2. по типу компонента/директивы: viewChild(ChildCmp) ← ищем <app-child>
3. + что вернуть (read): viewChild('chart', { read: ElementRef })
read уточняет, что именно отдать с найденного узла: сам элемент (ElementRef),
инстанс директивы, TemplateRef, ViewContainerRef и т.д.
View query vs Content query — главное различие
Это самая важная развилка. Разница — где искать:
viewChild / @ViewChild → в МОЁМ СОБСТВЕННОМ шаблоне
(то, что я написал в template: `...`)
contentChild / @ContentChild → в СПРОЕЦИРОВАННОМ контенте (<ng-content>)
(то, что мне передали между моими тегами снаружи)
Пример: компонент <app-card> со слотом <ng-content>.
<!-- внутри card.html (это VIEW компонента card): -->
<div class="card-header">...</div> ← сюда дотянется viewChild
<ng-content></ng-content> ← а сюда упадёт CONTENT
<!-- снаружи, родитель использует card: -->
<app-card>
<p #note>Заметка</p> ← это CONTENT для card → contentChild('note')
</app-card>
Связь с 13-content-projection: спроецированный контент принадлежит родителю, но живёт в дочернем — поэтому для него отдельный вид query.
Когда результат готов (ключевая ловушка)
Результат query появляется не сразу. Привязано к хукам из 11-lifecycle:
contentChild → готов в ngAfterContentInit (контент пришёл раньше)
viewChild → готов в ngAfterViewInit (свой шаблон собрался позже)
Поэтому классика ошибки: обратиться к @ViewChild в ngOnInit → там ещё undefined,
view ещё не построен.
Исключение — static: true: если искомый элемент статичен (не внутри @if/@for,
всегда есть), результат доступен раньше — уже в ngOnInit:
@ViewChild('header', { static: true }) header!: ElementRef; // есть в ngOnInit
@ViewChild('row') row!: ElementRef; // только в ngAfterViewInit
Под капотом: два прохода (TQuery + LQuery)
Как и везде в Ivy (см. 06-view-lview), работа делится на «бланк» и «копию»:
firstCreatePass (один раз на класс):
создаётся TQuery — СТАТИКА: что искать (предикат), какие узлы подходят.
Angular проходит шаблон и запоминает: «узел #5 совпал, читать ElementRef».
каждый change detection:
обновляется LQuery — РЕЗУЛЬТАТЫ: берём запомненные совпадения и материализуем
реальные объекты (ElementRef/инстанс) из текущего LView.
То есть «поиск» по сути происходит на компиляции + первом проходе, а в рантайме лишь собираются готовые объекты по заранее известным индексам. Никакого живого сканирования DOM (как и с селекторами в 07-selektory).
Множественные: children
viewChild ищет один (первый). Для списка — viewChildren / contentChildren:
items = viewChildren(ItemCmp); // Signal<readonly ItemCmp[]>
@ViewChildren(ItemCmp) old!: QueryList<ItemCmp>; // старый API
Старый API отдаёт QueryList — это обёртка-список с:
.changes— Observable, стреляет когда набор изменился (элементы добавились/убрали);- флаг
dirty— «надо пересобрать»; Angular сам зовётreset()при изменениях (@if/@forвставил/убрал view → query помечается грязным → пересобирается).
Сигнальные queries — новый способ
В Angular 21 рекомендованы функции-сигналы вместо декораторов:
// новое:
chart = viewChild<ElementRef>('chart'); // Signal<ElementRef | undefined>
chart = viewChild.required<ElementRef>('chart'); // Signal<ElementRef> (бросит, если нет)
rows = viewChildren(RowCmp); // Signal<readonly RowCmp[]>
// старое:
@ViewChild('chart') chart!: ElementRef;
Плюсы сигнальной версии:
- результат — обычный signal, читаешь
chart(), и он встроен в реактивность:computed/effectсами реагируют на появление/смену элемента; .requiredубираетundefinedиз типа (и кидает понятную ошибку, если не нашёл);- не нужно гадать про
staticи хуки — читаешь сигнал там, где он уже есть (например, вafterNextRenderилиeffect).
Под капотом сигнальный query — это computed, который пересчитывается, когда query
помечается грязным (тот же механизм dirty, что у QueryList, только завёрнут в сигнал).
Карта
ЧТО ИСКАТЬ ГДЕ КОГДА ГОТОВ
viewChild(...) #ref / тип / read свой шаблон ngAfterViewInit (или сигнал)
contentChild(...) #ref / тип / read <ng-content> ngAfterContentInit
*Children(...) то же то же то же, но список/QueryList
механизм: TQuery (статика, 1 раз) → LQuery (результаты, каждый CD)
поиск решён на компиляции; в рантайме — сбор готовых объектов
Итог
viewChild— в своём шаблоне;contentChild— в спроецированном контенте.- Результат готов в
ngAfterViewInit/ngAfterContentInit(или сразу приstatic: true); вngOnInitобычногоViewChildещё нет. - Под капотом —
TQuery(что искать, один раз) +LQuery(результаты, каждый CD), без живого сканирования DOM. - Предпочитай сигнальные
viewChild()/contentChild()— результат-сигнал,.required, дружит сcomputed/effect.
Дальше → 13-content-projection: что такое спроецированный контент, который ищет
contentChild.