ng-template, ng-container and ngTemplateOutlet - 全方位剖析 Angular 模板

ng-template, ng-container and ngTemplateOutlet - 全方位剖析 Angular 模板

在这篇文章中, 我们将会深入介绍一些 Angular Core 的高级功能!

你可能已经通过一些angular core 的指令间接的使用过 ng-template 了,例如 ngIf/else, ngSwitch.

ng-template 和 ngTemplateOutlet 命令是 angular 为我们提供的非常强大的功能为了支持更多样化的使用场景。

这些指令经常与 ng-container 一起使用,并且因为这些指令被设计为一起使用,所以我们这次将一起学习它们。

让我们来看看这些指令启用的一些更高级的用例。注意:这个帖子的所有代码都可以在这个GITHUB中找到。

ng-template 指令介绍

与名称相同,ng-template 指令 表示 Angular 模板:这意味着该标签的内容将包含模板的一部分,然后可以与其他模板组合在一起,以形成最终的组件模板。

Angular 已经在许多结构指令中悄悄地使用了 ng-template,我们经常使用的有:ngIf,ngFor 和 ngSwitch。

让我们用一个例子开始学习 ng-template。这里我们定义了选项卡组件的两个选项卡按钮(稍后对此进行详细说明):

@Component({
  selector: 'app-root',
  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模板标签只是定义了一个模板,但是我们现在还没有使用它。

先不要急,之后我们在其他例子中使模板输出。

ng-template 指令和 ngIf

你可能在第一次使用 ng-template 是在实现 if/else 场景时,例如:

"lessons-list" *ngIf="lessons else loading"> ...
#loading>
Loading...

这是 ngIf/else 功能的一个非常普遍的用法:在等待数据从后端返回时,我们显示了另一个 “loading“ 模板。

正如我们所看到的,esle 指向一个模板,该模板具有名称:loading,该名称通过模板引用被分配给它,使用的是 ”#loading“ 这样的语法。

但是除了 loading 模板之外,我们使用 ngIf 时还创建了第二个隐式 ng-template!让我们来看看语法糖包裹下到底发生了什么:

"lessons" [ngIfElse]="loading">
   
"lessons-list"> ...
#loading>
Loading...

这就是解开语法糖之后 Angular 所做的事情:
- 用结构指令 *ngIf 声明的元素移动到了 ng-template 中
- *ngIf 的表达式已经被分解并应用于两个独立的指令,使用[ngIf]和[ngIfElse]模板输入变量语法。

这是一个 ngIf 的例子,ngFor 和 ngSwitch 这两个语法糖其实原理也是一样的。

这些指令都是非常常用的,因此这意味着这些模板以隐式或显式的形式出现在 Angular 应用的各个地方。

但基于这个例子,可能会想到一个问题:
如果应用到同一个元素上有多个结构指令,它是如何工作的?

多结构指令

让我们看看如果我们尝试在同一个元素中使用 ngIf 和 ngFor会发生什么:

<div class="lesson" *ngIf="lessons" 
       *ngFor="let lesson of lessons">
    <div class="lesson-detail">
        {{lesson | json}}
    div>
div>  

这是行不通的!相反,我们会得到以下错误信息:

Uncaught Error: Template parse errors:
Can't have multiple template bindings on one element. Use only one attribute 
named 'template' or prefixed with *

这意味着不可能将两个结构指令应用到同一个元素。为了做到这一点,我们必须做一些类似的事情:

<div *ngIf="lessons">
    <div class="lesson" *ngFor="let lesson of lessons">
        <div class="lesson-detail">
            {{lesson | json}}
        div>
    div>
div>

在这个例子中,我们已经将 ngIf 指令移动到外层 div 元素上,但是为了使它工作,我们必须创建额外的 div 元素。

这个解决方案已经起作用了,但是有没有一种方法可以将结构指令应用到页面的一个部分,而不必创建额外的元素呢?

是的,这正是 ng-container 结构指令允许我们做的!

ng-container

为了避免创建额外的div,我们可以使用 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 为我们提供了一个元素,我们可以将结构指令附加到页面的一个部分,而不必为此创建额外的元素。

ng-container 还有另一个主要用途:它还可以提供一个占位符,用于将模板动态注入到页面中。

使用 ngTemplateOutlet 指令创建动态模板

能够创建模板引用、并将它们指向其他指令(例如ngIf)仅仅是开始。

我们也可以采用模板本身,并使用 ngTemplateOutlet 指令来实例化页面上的任何地方:

<ng-container *ngTemplateOutlet="loading">ng-container>

我们可以在这里看到 ng-container 如何使用ngTemplateOutlet:我们使用它来在页面上实例化上面定义的 #loading 模板。

我们通过模板引用加载来引用 #loading 模板,并且我们使用 ngTemplateOutlet 结构指令来实例化模板。

我们可以添加尽可能多的 ngTemplateOutlet 标签到我们想要的页面,并实例化一些不同的模板。传递给该指令的值可以是任何计算为模板引用的表达式,稍后将对此进行详细说明。

现在我们知道如何实例化模板,让我们来讨论一下模板可以访问哪些内容。

Template Context (模板的上下文)

关于模板的一个关键问题是,在它们里面什么是可见?

模板是否有自己的独立变量范围,模板可以看到哪些变量?

在 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:ctx">
ng-container>
`})
export class AppComponent {
    totalEstimate = 10;
    ctx = {estimate: this.totalEstimate}; 
}

下面我们对这个例子进行一下解析:
- 这个模板与前面的模板不同,它有一个输入变量(它也可以有几个)。
- 输入变量为 lessonsCounter,它是通过 ng-template 属性使用前缀 let- 定义的。
- 在 ng-template 内可见 lessonsCounter,但在外部不可见。
- 这个变量的内容是由其赋值给属性的表达式决定的。
- 该表达式是针对上下文对象进行求值的,与模板一起传递到 ngTemplateOutlet,以实例化
- 该上下文对象必须具有key为“estimate”的属性,以便在模板内显示其值。
- 上下文对象通过上下文属性传递给 ngTemplateOutlet,它可以接收任何对对象进行计算的表达式。

在上面的例子中,最终被渲染到屏幕上的事:

Approximately 10 lessons ...

我们还可以做的另一件事是在组件本身的层次上以编程方式与模板交互:让我们看看如何做到这一点。

Template References (模板引用)

同样,我们可以使用模板引用来引用加载模板,也可以使用 ViewChild 装饰器将一个模板直接注入到我们的组件中:

@Component({
  selector: 'app-root',
  template: `      
      #defaultTabButtons>
          
          
      
`})
export class AppComponent implements OnInit {
    @ViewChild('defaultTabButtons')
    private defaultTabButtonsTpl: TemplateRef;

    ngOnInit() {
        console.log(this.defaultTabButtonsTpl);
    }
}

正如我们所看到的,模板可以像任何其他DOM元素或组件一样注入,通过提供模板引用名称 defaultTabButtons 到 ViewChild 装饰器。

这意味着模板也可以在组件类的级别访问,并且我们可以做一些事情,例如将它们传递给子组件!

我们为什么要这样做呢?这里有一个例子是创建一个可定制的组件,可以传递给它的不仅仅是一个配置参数或配置对象,我们也可以传递一个模板作为一个输入参数。

可配置组件通过 @Input 导入部分模板

举一个Tab容器的例子,在这里我们想给组件的用户配置标签按钮。

下面这样的情况,我们首先定义父组件中按钮的自定义模板:

@Component({
  selector: 'app-root',
  template: `      
#customTabButtons>
    <div class="custom-class">
        <button class="tab-button" (click)="login()">
          {{loginText}}
        
        
    div>

container [headerTemplate]="customTabButtons">container>      
`})
export class AppComponent implements OnInit {
}

然后,在Tab容器组件上,我们可以定义一个输入属性,它也是一个名为 headerTemplate 的模板:

@Component({
    selector: 'tab-container',
    template: `
#defaultTabButtons>
    <div class="default-tab-buttons">
        ...
    div> 

"headerTemplate ? headerTemplate: defaultTabButtons">


... rest of tab container component ...
`})

export class TabContainerComponent {
    @Input()
    headerTemplate: TemplateRef;
}

在这最后一个结合的例子中,让我们来解析一下例子中都做了些什么:
- 对于选项卡按钮定义了默认模板,命名为defaultTabButtons
- 只有当输入属性标题模板未定义时,才会使用 defaultTabButtons 模板
- 如果定义了输入属性,则将通过 headerTemplate 模板传递的自定义输入模板用于显示按钮
- headerTemplate 模板通过 ngTemplateOutlet 属性在 ng-container 内实例化
- 使用三元表达式来决定使用哪个模板(默认或自定义),但是如果逻辑复杂,我们也可以把它委托给控制器方法。

此设计的最终结果是,如果没有提供自定义模板,Tab容器将显示选项卡按钮的默认外观,但如果提供了自定义模板,它将使用自定义模板。

总结

把 ng-container, ng-template 和 ngTemplateOutlet 指令都结合在一起,使我们能够创建高度动态和可定制的组件。

我们甚至可以完全改变基于输入模板的组件的外观,并且我们可以在应用程序的多个位置定义模板并实例化。

我希望这篇文章有助于熟悉 angular core 的一些更高级的特性,如果你有问题,请在下面的评论中告诉我,我会给你回复。

原文连接
https://blog.angular-university.io/angular-ng-template-ng-container-ngtemplateoutlet/

你可能感兴趣的:(angular)