0%

Angular ng-template、ng-container使用

我们可能已经在日常的开发中使用了ng-template的核心指令,比如ng-if/esle或者ngSwitchng-template指令和相关的ngTemplateOutlet指令是非常强大的Angular功能,支持各种高级用法。再搭配ng-container组合使用会非常方便和惊艳。

ng-template指令

就像名字一样,ng-template指令代表了一个Angular模板,它表示标签里面的内容是包含于一个模板,然后可以将其与其他模板组合,以形成最终的组件模板。

很多微语法的结构型指令在背后都是在使用ng-template,比如:*ngIf*ngFor*ngSwitch等。

比如,我们在展示页面的时候会有接口的请求,当接口请求中的时候页面展示loading,请求完毕后展示具体内容,我们可以先声明一个loading的模板:

1
2
3
4
5
<ng-template #loadingTemp>
<div class="loading">
<mat-spinner></mat-spinner>
</div>
</ng-template>

然后结合*ngIf来进行控制:

1
2
3
<div class="album-component" *ngIf="!loading else loadingTemp">
other
</div>

这是一种普遍的用法,微语法*ngIfelse子句指向一个模板,模板的名称为loadingTemp。这个名称是通过在模板上面声明模板引用变量产生的。

这个微语法其实分解开来是这样子的:

1
2
3
4
5
<ng-template [ngIf]="!loading" [ngIfElse]="loadingTemp">
<div>
other
</div>
</ng-template>

微语法给我们提供了很大的便利,我们来分析下*ngIf指令期间的变化:

  • 使用了*ngIf指令的元素会被移动到ng-template标签里面。
  • *ngIf指令的表达式被拆分为ngIfngIfElse两个模板输入变量

当使用*ngFor*ngSwitch的时候也会发生类似转换的过程。

那么,当我们在一个元素上使用两个结构指令的微语法会咋样?类似于:

1
2
3
4
5
6
7
<div *ngIf="!loading" *ngFor="let item of dataList" ></div>
...

只要看过官方文档,都是有一个意识的,就是两个结构型指令是不可以应用在同一个元素上的,会报错:
```
Uncaught Error: Template parse errors:
Can't have multiple template bindings on one element. Use only one attribute

我们需要拆开来展示:

1
2
3
<div *ngIf="!loading">
<div *ngFor="let item of dataList" ></div>
</div>

但这样会多出来一层div,在设置样式的时候会很别扭。

那么是不是有个方法我们可以不用增加额外的元素呢?那该祭出ng-container指令了。😏

ng-container

ng-container指令为我们提供了一个元素,我们可以将结构型指令应用在这个标签上,而不用增加额外的元素。

解决上面的多指令的问题:

1
2
3
<ng-container *ngIf="!loading">
<div *ngFor="let item of dataList" ></div>
</ng-container>

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

可以想象为一个模板的插槽,配合ngTemplateOutlet指令,我可以很方便的展示渲染模板。

我们前面创建的loadingTemp只有在*ngIf中判断条件后可以渲染,那么我们希望可以直接渲染,不需要任何判断,那么可以:

1
<ng-container *ngTemplateOutlet="loadingTemp"></ng-container>

模板上下文

关于模板的一个关键问题是,模板内部可见什么?

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

ng-template标记的主体内,我们可以访问外部模板中可见的上下文变量,也就是所有模板中可以访问的组件变量,在申明的模板内部都是可以访问的。

但是,每个模板也可以定义自己的一组输入变量。

实际上,每个模板都有一个关联的上下文对象,该上下文对象包含所有模板特定的输入变量。例如,我们有个相册列表:

1
2
3
4
5
6
7
8
9
10
11
<div class="album-list">
<div class="item" *ngFor="let item of albumList; trackBy: trackById">
<ng-container *ngTemplateOutlet="albumTemp; context: {album: item}"></ng-container>
</div>
</div>

<ng-template #albumTemp let-albumInfo="album">
<div class="image">
<img [src]="albumInfo.cover" (click)="show(albumInfo.id)"/>
</div>
</ng-template>

我们来看看这里发生的事情:

  • 创建了albumTemp模板,并声明接收模板变量album,命名为albumInfo
  • 模板输入变量名为albumInfo,通过ng-template的属性前缀let-来定义了这个变量。
  • 变量albumInfo在模板albumTemp内部可用,外部不可用(不污染组件变量空间)。
  • 变量的内容由let-albumInfo属性的表达式确定。
  • 表达式对上下文对象进行评估,语模板一起传递给ngTemplateOutlet进行实例化。
  • 然后上下文对象必须具有一个名为album的属性的对象,才能在模板中显示值。
  • 上下文对象通过ngTemplateOutlet指令的context属性传递给模板。

模板在组件中的使用

我们可以使用模板引用变量在组件的模板中使用,也可以使用ViewChild装饰器将模板在组件中使用:

1
@ViewChild('albumTemp') private albumTempRef: TemplateRef<any>;

这意味着模板可以在组件类中使用,那么我们也可以将它传递给子组件。这样的话,更方便我们创建定制化的组件,该组件不仅可以传递数据,也可以将模板传入。

假设我们有组件A,和子组件AChild,通过在A组件中定义模板userTemp,然后通过@Input传递给子组件AChild,然后在子组件中通过*ngTemplateOutlet来渲染模板。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {Component, Input, TemplateRef} from "@angular/core";

@Component({
selector: 'app-a',
styles: [],
template: `
<app-a-child [userTemp]="userTemp"></app-a-child>
<ng-template #userTemp>
<p>name</p>
</ng-template>
`,
})
export class AComponent {
}

@Component({
selector: 'app-a-child',
styles: [],
template: `
<ng-container *ngTemplateOutlet="userTemp"></ng-container>
`,
})
export class AChildComponent {
@Input() userTemp: TemplateRef<any> ;
}

这种设计的场景是,让组件的消费者可以自定义组件的内容,方便扩展。

参考链接:
https://blog.angular-university.io/angular-ng-template-ng-container-ngtemplateoutlet/

码字辛苦,打赏个咖啡☕️可好?💘