В 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 можно добиться очень многого. Практика в работе с вью и контентом позволит заметить, где эти знания помогут создавать надежные, легко поддерживаемые компоненты!

Комментарии (0)