Почему просто document.querySelector не работает в Angular?
Представьте: вы пишете своё первое приложение на Angular, и вам нужно получить доступ к обычному <div> или <input> прямо в шаблоне компонента. Первая мысль — использовать старый добрый document.querySelector. Но вот незадача: в консоли — null или undefined. Знакомо?
Angular — это не просто библиотека для манипуляций с DOM. Это фреймворк, который работает со своей собственной абстракцией над реальным DOM. Прямой доступ к элементам через глобальные селекторы нарушает архитектуру фреймворка, может привести к утечкам памяти и проблемам с производительностью. Но главная причина — Angular использует виртуальный DOM и Shadow DOM (в зависимости от стратегии инкапсуляции), поэтому ваш document.querySelector просто не видит элементов внутри компонента.
К счастью, разработчики Angular предусмотрели элегантное решение — декораторы ViewChild и ViewChildren. В этой статье мы разберём, как правильно выбирать элементы в шаблоне компонента, используя TypeScript, и рассмотрим все подводные камни.
Основные способы доступа к элементам шаблона
В Angular существует три основных подхода к выбору элементов в шаблоне компонента:
- Шаблонные ссылки (Template Reference Variables) — самый простой способ для быстрого доступа
- Декоратор ViewChild — для доступа к одному элементу или компоненту
- Декоратор ViewChildren — для доступа к коллекции однотипных элементов
Каждый из этих подходов имеет свои особенности и применяется в разных сценариях. Давайте рассмотрим их подробнее.
1. Шаблонные ссылки: самый быстрый способ
Шаблонные ссылки — это "лайфхак" от Angular, который позволяет получить доступ к DOM-элементу прямо в шаблоне, без необходимости писать код в классе компонента. Вы просто ставите #имяСсылки на нужном элементе.
// component.html
<input #myInput type="text" placeholder="Введите текст">
<button (click)="focusInput(myInput)">Фокус на инпут</button>
В этом примере #myInput — это шаблонная ссылка. Она автоматически создаёт переменную, которую можно передавать в методы компонента. Обратите внимание: мы передаём сам элемент, а не его значение.
// component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-example',
templateUrl: './example.component.html'
})
export class ExampleComponent {
focusInput(inputElement: HTMLInputElement): void {
inputElement.focus();
inputElement.style.borderColor = '#4CAF50';
}
}
Важный момент: шаблонные ссылки доступны только в пределах шаблона. Вы не можете использовать их в классе компонента напрямую. Для этого нужны декораторы.
2. ViewChild: доступ к одному элементу
Декоратор @ViewChild — это основной инструмент для получения доступа к элементу или дочернему компоненту из класса компонента. Он работает на основе селектора: вы можете искать по шаблонной ссылке, по типу компонента или по CSS-селектору (с ограничениями).
// component.ts
import { Component, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-example',
template: `
<div #myDiv>Этот элемент мы будем выбирать</div>
<p #myParagraph>И этот тоже</p>
`
})
export class ExampleComponent implements AfterViewInit {
@ViewChild('myDiv') myDiv!: ElementRef;
@ViewChild('myParagraph') myParagraph!: ElementRef;
ngAfterViewInit(): void {
// Важно! Доступ к элементам возможен только после инициализации представления
console.log(this.myDiv.nativeElement.textContent); // "Этот элемент мы будем выбирать"
this.myParagraph.nativeElement.style.color = 'red';
}
}
Обратите внимание на оператор ! (definite assignment assertion). Он сообщает TypeScript, что свойство будет инициализировано позже (Angular сделает это автоматически). Без него компилятор будет выдавать ошибку.
Также критически важно использовать хук ngAfterViewInit. На этапе ngOnInit шаблон ещё не отрисован, и @ViewChild вернёт undefined.
Выбор по типу компонента
ViewChild может выбирать не только DOM-элементы, но и другие компоненты. Это мощный инструмент для взаимодействия между родительским и дочерним компонентами.
// child.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-child',
template: '<p>Дочерний компонент</p>'
})
export class ChildComponent {
childMethod(): void {
console.log('Метод дочернего компонента вызван');
}
}
// parent.component.ts
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { ChildComponent } from './child.component';
@Component({
selector: 'app-parent',
template: `
<app-child></app-child>
<button (click)="callChildMethod()">Вызвать метод дочернего компонента</button>
`
})
export class ParentComponent implements AfterViewInit {
@ViewChild(ChildComponent) childComponent!: ChildComponent;
ngAfterViewInit(): void {
this.childComponent.childMethod(); // "Метод дочернего компонента вызван"
}
callChildMethod(): void {
this.childComponent.childMethod();
}
}
3. ViewChildren: работа с коллекцией элементов
Когда вам нужно получить доступ к нескольким однотипным элементам (например, всем <li> в списке), используйте @ViewChildren. Он возвращает объект QueryList, который можно итерировать.
// component.ts
import { Component, ViewChildren, QueryList, ElementRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-example',
template: `
<ul>
<li #item *ngFor="let item of items">{{ item }}</li>
</ul>
<button (click)="highlightFirst()">Подсветить первый элемент</button>
`
})
export class ExampleComponent implements AfterViewInit {
items = ['Angular', 'React', 'Vue', 'Svelte'];
@ViewChildren('item') itemsList!: QueryList;
ngAfterViewInit(): void {
// Перебор всех элементов
this.itemsList.forEach((item: ElementRef, index: number) => {
console.log(`Элемент ${index + 1}: ${item.nativeElement.textContent}`);
});
// Подписка на изменения
this.itemsList.changes.subscribe((list: QueryList) => {
console.log('Список элементов изменился!');
});
}
highlightFirst(): void {
const firstItem = this.itemsList.first;
if (firstItem) {
firstItem.nativeElement.style.backgroundColor = 'yellow';
}
}
}
QueryList предоставляет удобные методы: first, last, toArray(), forEach() и даже Observable changes, который срабатывает при динамическом изменении списка.