25. ViewChild и ContentChild
👁️ ViewChild, ContentChild и QueryList
Заголовок раздела «👁️ ViewChild, ContentChild и QueryList»ViewChild и ContentChild — инструменты для получения доступа к дочерним элементам из TypeScript. Это мощный, но требующий осторожности инструмент.
🎯 ViewChild — доступ к элементу View
Заголовок раздела «🎯 ViewChild — доступ к элементу View»ViewChild находит элемент в собственном шаблоне компонента:
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
@Component({ selector: 'app-input-focus', template: ` <input #myInput type="text" placeholder="Авто-фокус" /> <button (click)="focusInput()">Фокус</button> `})export class InputFocusComponent implements AfterViewInit { // Получаем ссылку на native DOM элемент @ViewChild('myInput') inputRef!: ElementRef<HTMLInputElement>;
ngAfterViewInit() { // ViewChild доступен только ПОСЛЕ ngAfterViewInit this.inputRef.nativeElement.focus(); }
focusInput() { this.inputRef.nativeElement.focus(); }}🔑 ViewChild с дочерним компонентом
Заголовок раздела «🔑 ViewChild с дочерним компонентом»Самое частое применение — вызов методов дочернего компонента:
@Component({ selector: 'app-timer', template: `<span>{{ time }}</span>`})export class TimerComponent { time = 0; private interval: any;
start() { this.interval = setInterval(() => this.time++, 1000); }
stop() { clearInterval(this.interval); }
reset() { this.stop(); this.time = 0; }}@Component({ selector: 'app-parent', template: ` <app-timer #timer /> <button (click)="timer.start()">▶ Старт</button> <button (click)="timer.stop()">⏸ Стоп</button> <button (click)="timerRef.reset()">↺ Сброс</button> `})export class ParentComponent implements AfterViewInit { @ViewChild('timer') timerRef!: TimerComponent; // или по типу: @ViewChild(TimerComponent) timer!: TimerComponent;
ngAfterViewInit() { console.log('Таймер готов:', this.timerRef); this.timerRef.start(); // вызываем метод дочернего компонента }}⏱️ static: true vs static: false
Заголовок раздела «⏱️ static: true vs static: false»// static: true — доступен с ngOnInit (нет *ngIf)@ViewChild('header', { static: true })headerRef!: ElementRef;
// static: false (по умолчанию) — доступен с ngAfterViewInit (есть *ngIf/*ngFor)@ViewChild('content', { static: false })contentRef!: ElementRef;Правило: если элемент обёрнут в *ngIf, *ngFor или ng-template — используй static: false.
ngOnInit() { // static: true — здесь уже доступен this.headerRef.nativeElement.textContent = 'Заголовок';}
ngAfterViewInit() { // static: false — только здесь this.contentRef?.nativeElement.classList.add('visible');}📖 Опция read — что читаем
Заголовок раздела «📖 Опция read — что читаем»import { ViewChild, ElementRef, TemplateRef, ViewContainerRef } from '@angular/core';
@ViewChild('myEl') // По умолчанию — компонент или ElementRef@ViewChild('myEl', { read: ElementRef }) // Всегда ElementRef (DOM)@ViewChild('myEl', { read: TemplateRef }) // ng-template ссылка@ViewChild('myEl', { read: ViewContainerRef }) // Контейнер для динамических компонентовПример с динамическим созданием компонента:
@Component({ template: `<ng-container #host></ng-container>`})export class DynamicHostComponent { @ViewChild('host', { read: ViewContainerRef }) host!: ViewContainerRef;
loadComponent() { this.host.clear(); this.host.createComponent(DynamicWidgetComponent); }}📋 ViewChildren — все совпадения
Заголовок раздела «📋 ViewChildren — все совпадения»ViewChildren возвращает QueryList<T> — живой список всех совпадений:
@Component({ template: ` <app-tab *ngFor="let tab of tabs" [title]="tab.title" /> `})export class TabsContainerComponent implements AfterViewInit { @ViewChildren(TabComponent) tabRefs!: QueryList<TabComponent>;
ngAfterViewInit() { console.log('Количество табов:', this.tabRefs.length);
// Итерируем this.tabRefs.forEach(tab => tab.initialize());
// Массив const tabArray = this.tabRefs.toArray();
// Следим за изменениями (добавление/удаление) this.tabRefs.changes.subscribe(tabs => { console.log('Список табов изменился:', tabs.length); }); }}🎭 ContentChild — проекция контента
Заголовок раздела «🎭 ContentChild — проекция контента»ContentChild находит элементы в проецируемом контенте (ng-content):
@Component({ selector: 'app-card', template: ` <div class="card"> <div class="header" *ngIf="cardHeader"> <ng-content select="[card-header]" /> </div> <div class="body"> <ng-content /> </div> </div> `})export class CardComponent implements AfterContentInit { @ContentChild('cardTitle') titleRef!: ElementRef; @ContentChild(CardHeaderDirective) cardHeader!: CardHeaderDirective;
ngAfterContentInit() { // ContentChild доступен с ngAfterContentInit if (this.titleRef) { console.log('Заголовок карточки:', this.titleRef.nativeElement.textContent); } }}<!-- Использование --><app-card> <h2 #cardTitle card-header>Мой заголовок</h2> <p>Содержимое карточки</p></app-card>📚 ContentChildren — все проецированные элементы
Заголовок раздела «📚 ContentChildren — все проецированные элементы»@Component({ selector: 'app-accordion', template: ` <div class="accordion"> <ng-content /> </div> `})export class AccordionComponent implements AfterContentInit { @ContentChildren(AccordionItemComponent) items!: QueryList<AccordionItemComponent>;
ngAfterContentInit() { // Подписываемся на изменения списка this.items.changes.subscribe(() => { this.updateItems(); }); this.updateItems(); }
private updateItems() { this.items.forEach((item, index) => { item.setIndex(index); item.toggle.subscribe(() => this.closeOthers(index)); }); }
private closeOthers(openIndex: number) { this.items.forEach((item, i) => { if (i !== openIndex) item.close(); }); }}⚡ QueryList — живой список
Заголовок раздела «⚡ QueryList — живой список»@ViewChildren(ItemComponent) items!: QueryList<ItemComponent>;
ngAfterViewInit() { // Свойства QueryList this.items.length // количество элементов this.items.first // первый элемент this.items.last // последний элемент this.items.toArray() // конвертация в массив this.items.get(2) // элемент по индексу this.items.find(i => i.id === 1) // поиск
// Observable изменений this.items.changes.subscribe((list: QueryList<ItemComponent>) => { console.log('Список обновлён'); });}export default function ViewChildPlayground() { const [timerRunning, setTimerRunning] = React.useState(false); const [time, setTime] = React.useState(0); const [childMessage, setChildMessage] = React.useState(''); const [log, setLog] = React.useState([]); const intervalRef = React.useRef(null); const childRef = React.useRef({ time: 0, running: false });
const addLog = (msg) => setLog(prev => [msg, ...prev].slice(0, 6));
// Simulate ViewChild access const parentCallsChildStart = () => { addLog('Parent: timerRef.start() — вызов метода через @ViewChild'); setTimerRunning(true); intervalRef.current = setInterval(() => { setTime(t => { childRef.current.time = t + 1; return t + 1; }); }, 1000); };
const parentCallsChildStop = () => { addLog('Parent: timerRef.stop() — вызов метода через @ViewChild'); clearInterval(intervalRef.current); setTimerRunning(false); };
const parentCallsChildReset = () => { addLog('Parent: timerRef.reset() — вызов метода через @ViewChild'); clearInterval(intervalRef.current); setTimerRunning(false); setTime(0); };
const parentReadsChildState = () => { addLog(`Parent: timerRef.time = ${time} — чтение свойства`); setChildMessage(`Прочитано из дочернего: time = ${time}`); };
React.useEffect(() => () => clearInterval(intervalRef.current), []);
const formatTime = (s) => `${String(Math.floor(s/60)).padStart(2,'0')}:${String(s%60).padStart(2,'0')}`;
return ( <div style={{ background: '#0f172a', minHeight: 440, padding: 24, borderRadius: 12, fontFamily: 'system-ui', color: '#e2e8f0' }}> <div style={{ color: '#dd0031', fontWeight: 700, fontSize: 16, marginBottom: 20 }}> 👁️ ViewChild: Parent → Child методы </div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 20 }}> {/* Parent */} <div style={{ background: '#1e293b', borderRadius: 10, padding: 16, border: '1px solid #7c3aed' }}> <div style={{ color: '#a78bfa', fontWeight: 700, fontSize: 13, marginBottom: 12 }}>🏠 ParentComponent</div> <div style={{ fontSize: 11, color: '#64748b', marginBottom: 12, fontFamily: 'monospace' }}> @ViewChild(TimerComponent) timerRef </div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}> <button onClick={parentCallsChildStart} disabled={timerRunning} style={{ background: timerRunning ? '#334155' : '#15803d', color: 'white', border: 'none', padding: '8px 14px', borderRadius: 8, cursor: timerRunning ? 'not-allowed' : 'pointer', fontSize: 13 }} > ▶ timerRef.start() </button> <button onClick={parentCallsChildStop} disabled={!timerRunning} style={{ background: !timerRunning ? '#334155' : '#7c2d12', color: 'white', border: 'none', padding: '8px 14px', borderRadius: 8, cursor: !timerRunning ? 'not-allowed' : 'pointer', fontSize: 13 }} > ⏸ timerRef.stop() </button> <button onClick={parentCallsChildReset} style={{ background: '#334155', color: '#94a3b8', border: 'none', padding: '8px 14px', borderRadius: 8, cursor: 'pointer', fontSize: 13 }} > ↺ timerRef.reset() </button> <button onClick={parentReadsChildState} style={{ background: '#1e3a5f', color: '#7dd3fc', border: '1px solid #1d4ed8', padding: '8px 14px', borderRadius: 8, cursor: 'pointer', fontSize: 13 }} > 📖 timerRef.time (read) </button> </div>
{childMessage && ( <div style={{ marginTop: 12, fontSize: 12, color: '#7dd3fc', background: '#0f172a', padding: '8px', borderRadius: 6 }}> {childMessage} </div> )} </div>
{/* Child */} <div style={{ background: '#1e293b', borderRadius: 10, padding: 16, border: `2px solid ${timerRunning ? '#dd0031' : '#334155'}`, transition: 'border-color 0.3s' }}> <div style={{ color: '#dd0031', fontWeight: 700, fontSize: 13, marginBottom: 12 }}> 👶 TimerComponent <span style={{ fontSize: 10, color: '#64748b', marginLeft: 8, fontFamily: 'monospace' }}> #timer="timerRef" </span> </div>
<div style={{ textAlign: 'center', padding: '20px 0' }}> <div style={{ fontSize: 48, fontWeight: 700, color: timerRunning ? '#dd0031' : '#e2e8f0', fontFamily: 'monospace', transition: 'color 0.3s' }}> {formatTime(time)} </div> <div style={{ fontSize: 12, color: timerRunning ? '#22c55e' : '#64748b', marginTop: 8 }}> {timerRunning ? '● Работает' : '● Остановлен'} </div> </div>
<div style={{ background: '#0f172a', borderRadius: 6, padding: 10, fontSize: 11, fontFamily: 'monospace' }}> <div style={{ color: '#64748b', marginBottom: 4 }}>// Публичные методы/свойства:</div> <div style={{ color: '#7dd3fc' }}>time: <span style={{ color: '#a3e635' }}>{time}</span></div> <div style={{ color: '#7dd3fc' }}>running: <span style={{ color: '#a3e635' }}>{String(timerRunning)}</span></div> <div style={{ color: '#64748b', marginTop: 4 }}>start() / stop() / reset()</div> </div> </div> </div>
{/* Log */} <div style={{ background: '#1e293b', borderRadius: 8, padding: 14, border: '1px solid #334155', marginTop: 16 }}> <div style={{ color: '#7dd3fc', fontSize: 12, marginBottom: 8 }}>📋 @ViewChild вызовы</div> {log.length === 0 && <div style={{ color: '#475569', fontSize: 12 }}>Нажмите кнопки родителя...</div>} {log.map((l, i) => ( <div key={i} style={{ fontSize: 12, color: i === 0 ? '#e2e8f0' : '#475569', marginBottom: 3, transition: 'color 0.5s' }}>{l}</div> ))} </div> </div> );}