Директивы ng-template, ngTemplateOutlet и ng-container
Директивы ng-template
и связанная ngTemplateOutlet
очень мощные инструменты Angular, которые часто используются с ng-container
.
Angular директива ng-template
ng-template
директива 'отрисовывает' Angular шаблон: это означает, что содержимое этого тега будет содержать часть шаблона, которая затем может быть использована вместе с другими шаблонами для формирования окончательного шаблона компонента.
Директива ng-template
используется под капотом в ngIf
, ngFor
и ngSwitch
директивах.
Давайте начнем изучение ng-template
на примере. Определим два таба (кнопки) компонента таба (больше - дальше).
@Component({
selector: 'app-root',
template: `
<ng-template>
<button class="tab-button"
(click)="login()">{{loginText}}</button>
<button class="tab-button"
(click)="signUp()">{{signUpText}}</button>
</ng-template>
`})
export class AppComponent {
loginText = 'Login';
signUpText = 'Sign Up';
lessons = ['Lesson 1', 'Lessons 2'];
login() {
console.log('Login');
}
signUp() {
console.log('Sign Up');
}
}
ng-template
ничего не рендерит, мы просто определяем шаблон, но пока не используем его.ng-template директива и ngIf
Возможно, вы впервые столкнулись с реализацией ng-template
в сценарии if/else
, например:
<div class="lessons-list" *ngIf="lessons else loading">
...
</div>
<ng-template #loading>
<div>Loading...</div>
</ng-template>
Очень частый случай: мы показываем альтернативный шаблон loading
пока данные не будут получены с бэка. Как вы видите условие else
указывает на шаблон, который имеет имя loading
. Имя привязано через переменную шаблона #loading
.
Помимо шаблона для else
, использование ngIf
также неявно создает второй ng-template
! Давайте взглянем на то что происходит под капотом:
<ng-template [ngIf]="lessons" [ngIfElse]="loading">
<div class="lessons-list">
...
</div>
</ng-template>
<ng-template #loading>
<div>Loading...</div>
</ng-template>
*ngIf
имеет более лаконичный синтаксис. Что же происходит под капотом *ngIf
:
- элемент, к которому была применена структурная директива был перемещен в
ng-template
- выражение
*ngIf
было разделено на две отдельные директивы[ngIf]
и[ngIfElse]
с использованиемInput
синтаксиса
ngFor
и ngSwitch
работают схожим образом.
Отметьте, что мы не можем использовать несколько структурных директив на одном элементе.
Директива ng-container
Директива ng-container
позволяет применить структурную директиву к разделу страницы, не создавай при этом дополнительный элемент (тег).
Итак, чтобы не создавать дополнительный div
мы можем воспользоваться директивой ng-container
(в разметке тега ng-container
вы не увидите) и уже на ней применить структурную директиву:
<ng-container *ngIf="lessons">
<div class="lesson" *ngFor="let lesson of lessons">
<div class="lesson-detail">
{{lesson | json}}
</div>
</div>
</ng-container>
Есть еще важная черта директивы ng-container
: она может предоставить заполнитель для инжектирования динамического шаблона на страницу.
Создание динамических шаблонов с директивой ngTemplateOutlet
Возможность создавать ссылки на шаблоны и указывать их другие директивы, такие как ngIf
это только начало.
Мы также можем взять сам шаблон и создать его экземпляр где угодно на странице, используя ngTemplateOutlet
директиву:
<ng-container *ngTemplateOutlet="loading"></ng-container>
Здесь мы используем ng-container
и структурный директиву ngTemplateOutlet
для создания шаблона loading
, который мы определили выше при помощи переменной шаблона #loading
.
Контекст шаблона
Один вопрос насчет шаблона - что мы видим внутри него? Имеет ли шаблон свою собственную область видимости? Какие переменный может видеть шаблон?
Внутри тела ng-template
мы имеем доступ к тому же контексту, который виден во внешнем шаблоне, например, переменной lessons
(то есть ng-template
экземпляр имеет доступ к тому же контексту, в которой он встроен).
Но каждый шаблон также может определить свой собственный набор входящих переменных! Фактически, каждый шаблон имеет связанный объект контекста, содержащий все входные переменные специфичный для шаблоны.
Давайте рассмотрим пример:
@Component({
selector: 'app-root',
template: `
<ng-template #estimateTemplate let-lessonsCounter="estimate">
<div> Approximately {{lessonsCounter}} lessons ...</div>
</ng-template>
<ng-container
*ngTemplateOutlet="estimateTemplate; context: templateCtx"></ng-container>
`})
export class AppComponent {
totalEstimate = 10;
templateCtx = {
estimate: this.totalEstimate
};
}
Особенности:
- входящая переменная названа
lessonsCounter
, и определена вng-template
через префиксlet-
- переменная
lessonsCounter
видна внутриng-template
, но не снаружи - значение переменной
lessonsCounter
равно выражение, которое присвоеноlet-lessonsCounter
- это выражение берется по объекту контекста, который передан
ngTemplateOutlet
вместе с шаблоном - объект контекста должен иметь свойство
estimate
, чтобы его значение отображалось внутри шаблона - объект контекста передан
ngTemplateOutlet
через свойствоcontext
Пример выше отрендерит:
Approximately 10 lessons ...
Отличный пример того как определять и создавать наши собственные шаблоны.
Переменные шаблона
Декоратор @ViewChild
позволяет нам получить доступ к дочернему компоненту из родительского компонента.
Настраиваем компоненты с Частичными Шаблонами @Inputs
Возьмем таб контейнер и разрешим пользователям настраивать внешний вид кнопок вкладок (в контексте статьи автор просто передает через декоратор Input
'пользовательский' шаблон с кнопками дочернему компоненту; если его не передать будет использован дефолтный шаблон с кнопками).
Определим шаблон с кнопками в родительском компоненте:
@Component({
selector: 'app-root',
template: `
<ng-template #customTabButtons>
<div class="custom-class">
<button class="tab-button" (click)="login()">
{{loginText}}
</button>
<button class="tab-button" (click)="signUp()">
{{signUpText}}
</button>
</div>
</ng-template>
<tab-container [headerTemplate]="customTabButtons"></tab-container>
`})
export class AppComponent implements OnInit {
}
Затем в компоненте таб контейнера определим входящее свойство, котороя также является шаблоном с именем headerTemplate
.
@Component({
selector: 'tab-container',
template: `
<ng-template #defaultTabButtons>
<div class="default-tab-buttons">
...
</div>
</ng-template>
<ng-container
*ngTemplateOutlet="headerTemplate ? headerTemplate: defaultTabButtons">
</ng-container>
... rest of tab container component ...
`})
export class TabContainerComponent {
@Input()
headerTemplate: TemplateRef<any>;
}
Пара вещей, которые стоит отметить:
- шаблон по умолчанию для кнопок назван
defaultTabButtons
- этот шаблон будет использован, если входящее свойство
headerTemplate
неundefined
- если свойство определено, то пользовательский входящий шаблон переданные через
headerTemplate
будет использован для показа кнопок - шаблон с кнопками создается внутри
ng-container
и с использованиемngTemplateOutlet
По сути мы будем использовать пользовательский шаблон, если он есть, или шаблон по умолчанию.
Заключение
Корневые директивы ng-container
, ng-template
и ngTemplateOutlet
объединяются вместе чтобы позволить нам создавать высоко динамичные и настраиваемые компоненты.