ContentChild, ViewChild, template reference variables

В Angular принято писать декларативный код. Это значит, что нам не стоит руками запрашивать нужные нам сущности. Во фреймворке есть инструменты для работы с элементами шаблона, которые помогут нам. О них сегодня и поговорим.

bae523ffd04cfeac77fd7387e2c0e069.png

Кто есть кто

Для начала давайте разберемся, что такое вью и что такое контент.

Вью — это шаблон нашего компонента, директивы его не имеют.

Контент — это то, что оборачивает наш компонент или директива.

Для компонентов потребуется добавить тег ng-content в шаблон, иначе все содержимое заменится на шаблон компонента при рендере.

Совет по производительности: даже если ng-content спрятан за *ngIf и не прикреплен к документу, он все равно рендерится и проходит все циклы проверки изменений. Если нужна ленивая инициализация — используем ng-template.

ViewChild

Когда мы создаем компонент, может потребоваться доступ к частям его шаблона. Его можно получить через декоратор @ViewChild. Для начала нужно предоставить селектор — можно пометить элемент строкой:

...

А затем запросить его через декоратор: @ViewChild(‘ref’).

#ref называется template reference variable, и далее мы обсудим их детально.

Можно также использовать класс, если вам нужно получить компонент или директиву: @ViewChild(MyComponent). Это может быть DI-токен: @ViewChild(MY_TOKEN).

Запросы через декораторы обрабатываются в рамках лайфсайкл-хуков, ngOnInit, ngAfterViewInit и других. Подробнее о них — в следующей статье.

Особенно полезен второй аргумент декоратора — объект параметров. Первый ключ простой — static: boolean. Он говорит Angular, что некий элемент существует всегда, а не по условию:

  • static: true означает, что этот элемент будет доступен уже в ngOnInit;

  • static: false означает, что элемент появится только в ngAfterViewInit, когда весь шаблон будет проверен.

Значение по умолчанию — false, значит, результат будет получен только после прохождения первой проверки изменений.

Я статичный ребёнок
Я не статичный ребёнок

Замечу, что даже статичные запросы дают результат только в ngOnInit, так что технически некоторое время значение равно undefined. Хорошая практика — отмечать это в типах, чтобы случайно не обратиться к ним в конструкторе.

Второй ключ — read. Первый аргумент декоратора говорит Angular: «найди мне инжектор, в котором есть данная сущность», а read говорит, что из этого инжектора взять. Если его не писать, то мы получим сам токен из первого аргумента.

Можно запросить любую сущность из целевого инжектора: сервис, токен, компонент и так далее. Чаще всего это используется, чтобы получить DOM-элемент целевого компонента:

@ViewChild(MyComponent, { read: ElementRef })
readonly elementRef?: ElementRef

Template Reference Variables

Часто в @ViewChild вовсе нет нужды — во многих случаях отлично подойдет template reference variable и передача ее в обработчик события:


"
// Обратите внимание, что тут HTMLElement, а не ElementRef
onClick(element: HTMLElement) {
   element.focus();
}

Можно рассматривать template reference variable как некое замыкание в шаблоне — ссылка есть, но доступна только там, где нужна. Так код компонента остается чистым.

Template reference variable обращается к инстансу компонента, если поместить ее на него или к DOM-элементу, если компонента там нет. А еще можно получить сущность директивы, для этого используется exportAs в декораторе @Directive:

@Directive({
 selector: '[myDirective]',
 exportAs: 'someName',
})
export class MyDirective {}
...

ViewChildren

Иногда нужно получить множество элементов одного типа. В такой ситуации можно использовать @ViewChildren — отметить множество элементов в шаблоне одной и той же строчкой и получить всю коллекцию.

Все вышесказанное применимо и тут, только static недоступен для списков и типом поля будет QueryList. Это позволяет пройтись по каждому элементу при необходимости. Рассмотрим пример компонента вкладок: вместо горизонтального скролла хотим спрятать лишние вкладки в пункт «Еще». В StackBlitz ниже @ViewChildren используется для подобной реализации:

Контент

Удобным способом кастомизации компонентов может стать контент. В Angular он напоминает слоты из нативных веб-компонентов и позволяет проецировать содержимое в разные участки шаблона с помощью тега ng-content.

ng-content позволяет гибко раскидать части содержимого в разные места с помощью атрибута select. Его синтаксис похож на selector в директивах и компонентах — можно завязываться на имена тегов, классы, атрибуты и комбинировать это всевозможным образом, вплоть до отрицания через :not(). Всегда можно оставить ng-content без селектора, чтобы все остальное содержимое угодило в него.

Важно помнить, что хоть контент и находится по DOM внутри вьюхи компонента, на самом деле он является частью родительского вью и следует его циклам проверки изменений. Это значит, что если элемент в контенте будет помечен для проверки, к примеру, через наступление события из @HostListener, то вью оборачивающего контент компонента проверен не будет. Поэтому если компонент зависит от контента, убедитесь, что вы не пропустите изменения в нем.

changes: Observable из QueryList поможет уследить за изменениями списков в контенте.

ContentChild и ContentChildren

Обращаться к контенту можно так же, как к шаблону компонента. В Angular есть аналогичные декораторы @ContentChild и @ContentChildren с тем же синтаксисом.

Вот пример компонента меню, в котором элементы передаются в виде контента. Это позволяет разработчику завязаться на события, такие как клики или нажатия клавиш, не забивая этим логику меню. Сам же компонент отвечает за навигацию с клавиатуры.

Кое-что в синтаксисе для декораторов контента отличается от вью. В них появляется дополнительный параметр опций descendants: boolean. Он позволяет получать детей из контента, находящихся в контенте другого вложенного компонента, но не в их вью:



 
   
...

С такими возможностями в Angular можно добиться очень многого. Практика в работе с вью и контентом позволит заметить, где эти знания помогут создавать надежные, легко поддерживаемые компоненты!

© Habrahabr.ru