Перейти к содержимому

25. ViewChild и ContentChild

ViewChild и ContentChild — инструменты для получения доступа к дочерним элементам из TypeScript. Это мощный, но требующий осторожности инструмент.


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();
}
}

Самое частое применение — вызов методов дочернего компонента:

child.component.ts
@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;
}
}
parent.component.ts
@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 — доступен с 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');
}

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 возвращает 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 находит элементы в проецируемом контенте (ng-content):

card.component.ts
@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();
});
}
}

@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>
);
}