Директивы ng-template, ngTemplateOutlet и ng-container

Mar 8, 2019

Директивы ng-template и связанная ngTemplateOutlet очень мощные инструменты Angular, которые часто используются с ng-container.

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 ...

Отличный пример того как определять и создавать наши собственные шаблоны.

Также стоит обратить внимание на статья с сайта tyapk: let-* $implicit in Angular template

Переменные шаблона

Декоратор @ViewChild позволяет нам получить доступ к дочернему компоненту из родительского компонента.

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 объединяются вместе чтобы позволить нам создавать высоко динамичные и настраиваемые компоненты.

Источник
Там же есть ссылка на github.

Добавить комментарий