В Angular принято писать декларативный код. Это значит, что нам не стоит руками запрашивать нужные нам сущности. Во фреймворке есть инструменты для работы с элементами шаблона, которые помогут нам. О них сегодня и поговорим.
Кто есть кто
Для начала давайте разберемся, что такое вью и что такое контент.
Вью — это шаблон нашего компонента, директивы его не имеют.
Контент — это то, что оборачивает наш компонент или директива.
Для компонентов потребуется добавить тег ng-content
в шаблон, иначе все содержимое заменится на шаблон компонента при рендере.
Совет по производительности: даже если ng-content спрятан за *ngIf
и не прикреплен к документу, он все равно рендерится и проходит все циклы проверки изменений. Если нужна ленивая инициализация — используем ng-template
.
ViewChild
Когда мы создаем компонент, может потребоваться доступ к частям его шаблона. Его можно получить через декоратор @ViewChild
. Для начала нужно предоставить селектор — можно пометить элемент строкой:
<div #ref>...</div>
А затем запросить его через декоратор: @ViewChild(‘ref’)
.
#ref
называется template reference variable, и далее мы обсудим их детально.
Можно также использовать класс, если вам нужно получить компонент или директиву: @ViewChild(MyComponent)
. Это может быть DI-токен: @ViewChild(MY_TOKEN)
.
Запросы через декораторы обрабатываются в рамках лайфсайкл-хуков, ngOnInit
, ngAfterViewInit
и других. Подробнее о них — в следующей статье.
Особенно полезен второй аргумент декоратора — объект параметров. Первый ключ простой — static: boolean
. Он говорит Angular, что некий элемент существует всегда, а не по условию:
static: true
означает, что этот элемент будет доступен уже вngOnInit
;static: false
означает, что элемент появится только вngAfterViewInit
, когда весь шаблон будет проверен.
Значение по умолчанию — false
, значит, результат будет получен только после прохождения первой проверки изменений.
<div #static>
Я статичный ребёнок
</div>
<div *ngIf="true" #dynamic>
Я не статичный ребёнок
</div
Замечу, что даже статичные запросы дают результат только в ngOnInit
, так что технически некоторое время значение равно undefined
. Хорошая практика — отмечать это в типах, чтобы случайно не обратиться к ним в конструкторе.
Второй ключ — read
. Первый аргумент декоратора говорит Angular: «найди мне инжектор, в котором есть данная сущность», а read
говорит, что из этого инжектора взять. Если его не писать, то мы получим сам токен из первого аргумента.
Можно запросить любую сущность из целевого инжектора: сервис, токен, компонент и так далее. Чаще всего это используется, чтобы получить DOM-элемент целевого компонента:
@ViewChild(MyComponent, { read: ElementRef })
readonly elementRef?: ElementRef<HTMLElement>
Template Reference Variables
Часто в @ViewChild
вовсе нет нужды — во многих случаях отлично подойдет template reference variable и передача ее в обработчик события:
<input #input type="text" [(ngModel)]="model">
<button (click)="onClick(input)">Focus input</button>"
// Обратите внимание, что тут HTMLElement, а не ElementRef
onClick(element: HTMLElement) {
element.focus();
}
Можно рассматривать template reference variable как некое замыкание в шаблоне — ссылка есть, но доступна только там, где нужна. Так код компонента остается чистым.
Template reference variable обращается к инстансу компонента, если поместить ее на него или к DOM-элементу, если компонента там нет. А еще можно получить сущность директивы, для этого используется exportAs
в декораторе @Directive
:
@Directive({
selector: '[myDirective]',
exportAs: 'someName',
})
export class MyDirective {}
<div myDirective #ref="someName">...</div>
ViewChildren
Иногда нужно получить множество элементов одного типа. В такой ситуации можно использовать @ViewChildren
— отметить множество элементов в шаблоне одной и той же строчкой и получить всю коллекцию.
Все вышесказанное применимо и тут, только static
недоступен для списков и типом поля будет QueryList<T>
. Это позволяет пройтись по каждому элементу при необходимости. Рассмотрим пример компонента вкладок: вместо горизонтального скролла хотим спрятать лишние вкладки в пункт «Еще». В StackBlitz ниже @ViewChildren
используется для подобной реализации:
Контент
Удобным способом кастомизации компонентов может стать контент. В Angular он напоминает слоты из нативных веб-компонентов и позволяет проецировать содержимое в разные участки шаблона с помощью тега ng-content
.
ng-content
позволяет гибко раскидать части содержимого в разные места с помощью атрибута select
. Его синтаксис похож на selector
в директивах и компонентах — можно завязываться на имена тегов, классы, атрибуты и комбинировать это всевозможным образом, вплоть до отрицания через :not()
. Всегда можно оставить ng-content
без селектора, чтобы все остальное содержимое угодило в него.
Важно помнить, что хоть контент и находится по DOM внутри вьюхи компонента, на самом деле он является частью родительского вью и следует его циклам проверки изменений. Это значит, что если элемент в контенте будет помечен для проверки, к примеру, через наступление события из @HostListener
, то вью оборачивающего контент компонента проверен не будет. Поэтому если компонент зависит от контента, убедитесь, что вы не пропустите изменения в нем.
changes: Observable<void>
из QueryList
поможет уследить за изменениями списков в контенте.
ContentChild и ContentChildren
Обращаться к контенту можно так же, как к шаблону компонента. В Angular есть аналогичные декораторы @ContentChild
и @ContentChildren
с тем же синтаксисом.
Вот пример компонента меню, в котором элементы передаются в виде контента. Это позволяет разработчику завязаться на события, такие как клики или нажатия клавиш, не забивая этим логику меню. Сам же компонент отвечает за навигацию с клавиатуры.
Кое-что в синтаксисе для декораторов контента отличается от вью. В них появляется дополнительный параметр опций descendants: boolean
. Он позволяет получать детей из контента, находящихся в контенте другого вложенного компонента, но не в их вью:
<!-- @ContentChild('child', { descendants: true }) -->
<my-component>
<another-component>
<div #child>...</div>
</another-component>
</my-component>
С такими возможностями в Angular можно добиться очень многого. Практика в работе с вью и контентом позволит заметить, где эти знания помогут создавать надежные, легко поддерживаемые компоненты!