Angular指令之ngTemplateOutlet解析

前提

假设你已了解Angular框架的基本使用,知晓指令等相关概念及作用。

功能

ngTemplateOutlet:根据一个提前备好的TemplateRef插入一个内嵌视图。

用法

平常在使用一些第三方库组件的时候,比如ng-zorro中的Rate评分组件,查看api文档时会发现其中nzCharacter属性可以传递一个TemplateRef类型值,从而实现用户自定义功能。

属性说明类型默认值
[nzCharacter]自定义字符TemplateRef<i nz-icon nzType="star"></i>

demo:

image.png

import { Component } from '@angular/core';

@Component({ selector: 'nz-demo-rate-character',
template: `
    <nz-rate [ngModel]="0" nzAllowHalf [nzCharacter]="characterIcon"></nz-rate>
    <ng-template #characterIcon let-num><i nz-icon nzType="heart"></i></ng-template> 
`,
styles: []
})
export class NzDemoRateCharacterComponent {} 

那他是怎么实现的,为什么传递一个TemplateRef类型模板就会实现自定义,看一下它的源码 因为评分列表有多个item,所以nzCharacter最终作用到NzRateItemComponent组件上:

<div class="ant-rate-star-second" (mouseover)="hoverRate(false); $event.stopPropagation()"
(click)="clickRate(false)">
    <ng-template
      [ngTemplateOutlet]="character || defaultCharacter"
      [ngTemplateOutletContext]="{ $implicit: index }"
    ></ng-template>
</div>

<div class="ant-rate-star-first" (mouseover)="hoverRate(true); $event.stopPropagation()" (click)="clickRate(true)">
    <ng-template
      [ngTemplateOutlet]="character || defaultCharacter"
      [ngTemplateOutletContext]="{ $implicit: index }"
    ></ng-template>
</div>

<ng-template #defaultCharacter>
<i nz-icon nzType="star" nzTheme="fill"></i>
</ng-template> 

如上所示我们发现character属性赋值给了ngTemplateOutlet

综述上层用户自定义的模板(这里的小心心)作为扩展传递给评分组件,组件通过ngTemplateOutlet从而实现了用户可定制化功能。

让我们继续剖析ngTemplateOutlet是怎么实现的。

ngTemplateOutlet源码分析(v13.3.x)

其实ngTemplateOutlet是一个angular框架的内置结构指令。源码地址

它在common包的directives文件夹中,所以说要想使用这个指令,项目module中必须引入CommonModule,当然CommonModule还有很多东西,它实现Angular框架的基本功能,包括指令和管道、路由中使用的位置服务、HTTP服务、本地化支持等等,这个以后再进行介绍。(ps:如果你是使用ng g m xxx脚手架生成,cli会默认将CommonModule导入到xxx.module中,开发时内置组件指令使用报错可以检查一下是否不小心删掉了CommonModule这是一个小技巧)。

言归正传,我们看一下ngTemplateOutlet的实现,代码不多,直接贴出来方便阅读:

import {Directive, EmbeddedViewRef, Injector, Input, OnChanges, SimpleChanges, TemplateRef, ViewContainerRef} from '@angular/core';

/**
 * @ngModule CommonModule
 *
 * @description
 *
 * 根据一个提前备好的 TemplateRef 插入一个内嵌视图。
 *
 * 你可以通过设置 [ngTemplateOutletContext] 来给 EmbeddedViewRef 附加一个上下文对象。
 * [ngTemplateOutletContext] 是一个对象,该对象的 key 可在模板中使用 let 语句进行绑定。
 *
 * @usageNotes
 * ```html
 * <ng-container *ngTemplateOutlet="templateRefExp; context: contextExp"></ng-container>
 * ```
 * 在上下文对象中使用 $implicit 这个 key 会把对应的值设置为默认值
 *
 */
@Directive({ selector: '[ngTemplateOutlet]' })
export class NgTemplateOutlet implements OnChanges {
  private _viewRef: EmbeddedViewRef<any> | null = null;

  /**
   * 附加到 {@link EmbeddedViewRef} 的上下文对象。这应该是一个对象,该对象的键名将可以在局部模板中使用 let 声明中进行绑定。
   * 在上下文对象中使用 $implicit 为键名时,将把它作为默认值
   */
  @Input() public ngTemplateOutletContext: Object | null = null;

  /**
   * 一个字符串,用于定义模板引用以及模板的上下文对象
   */
  @Input() public ngTemplateOutlet: TemplateRef<any> | null = null;

  constructor(private _viewContainerRef: ViewContainerRef) { }

  /** @nodoc */
  ngOnChanges(changes: SimpleChanges) {
    if (changes['ngTemplateOutlet']) {
      const viewContainerRef = this._viewContainerRef;

      if (this._viewRef) {
        viewContainerRef.remove(viewContainerRef.indexOf(this._viewRef));
      }

      this._viewRef = this.ngTemplateOutlet ?
        viewContainerRef.createEmbeddedView(this.ngTemplateOutlet, this.ngTemplateOutletContext) :
        null;
    } else if (
      this._viewRef && changes['ngTemplateOutletContext'] && this.ngTemplateOutletContext) {
      this._viewRef.context = this.ngTemplateOutletContext;
    }
  }
} 

个人还是觉得angular源码注释做的还是很好(上述注释给翻译了一下),我们应该养成写注释的好习惯。

我们发现NgTemplateOutlet类实现了OnChanges钩子函数,目的是当@Input属性值发生改变时能够进行创建新的视图。它有两个输入属性:ngTemplateOutletngTemplateOutletContextngTemplateOutlet就是TemplateRef类型的模板片段,ngTemplateOutletContext是一个的对象,他来给 EmbeddedViewRef(内嵌式视图,也就是自定义的ng-template) 附加一个上下文,该对象的 key 可在模板中使用 let 语句进行绑定。

例如刚才的评分组件,如果[ngTemplateOutletContext]="{ $implicit: index ;a:1}"就可以将index的值,a=1传递给上层的<ng-tempalte>

<ng-tempalte let-num let-xxx="a">{{num}}{{xxx}}</ng-tempalte> 

用let定义两个变量分别是num和xxx,这将会获得num(实际组件内部的index)和xxx(实际a的值也就是1)的值。这么做的好处是,组件内部状态可以传递出去,让上层获取到想要的数据。

这里有个小技巧,$implicit作为默认key,模板中用let-num定义变量时默认取$implicit对应的值,其他key值需要被赋值:let-xxx=‘a’。

实现逻辑

本质使用ViewContainerRef里的createEmbeddedView()方法创建内嵌式视图。ViewContainerRef不仅是创建内嵌式视图,还可以用来创建组件。后续会介绍ViewContainerRef

代码逻辑:

  1. 如果ngTemplateOutlet有输入
  2. 先判断当前容器内有没有视图(_viewRef),如果有先清空视图容器
  3. 调用createEmbeddedView()创建内嵌式视图
  4. 如果当前视图容器不为空且ngTemplateOutletContext有输入,则给创建的视图(_viewRef)添加上下文

简写形式

<ng-container *ngTemplateOutlet="temp;context:context;"></ng-container>
这是ng的简写形式,最终由编译器解开等同于下面的格式 
<ng-container [ngTemplateOutlet]="temp" [ngTemplateOutletContext]="context"></ng-container> 
// 使用简写形式确保都是以ngTemplateOutlet开头
@Directive({ selector: '[ngTemplateOutlet]' })
@Input() public ngTemplateOutlet: TemplateRef<any> | null = null;
@Input() public ngTemplateOutletContext: Object | null = null; 

思维扩展

可以用投影的方式实现组件扩展,类似于:

<a-component>
  <ng-template>hi,Enoch Gao!</ng-template>
</a-component> 

a-component内部通过@ContentChild注解获取TemplateRef也是一种比较常见扩展实现方式。

TemplateRef的获取方式有多种方式:

  1. 可以通过@ViewChild@ViewChildren@ContentChild@ContentChildren查询的方式
  2. 也可以<ng-template #temp>定义变量的形式
  3. 还可以自定义指令
<ng-template appCustomDir>我是扩展内容</ng-template> 

通过appCustomDir指令构造函数中注入constructor(private temp:TemplateRef){}获取到TemplateRef 4. …

万变不离其宗,只要我们获取到了TemplateRef就可以实现扩展。

总结

通过内置指令ngTemplateOutlet可以实现组件定制化功能,在我们写通用组件的时候可提高组件的规范化,定制化能力。

后续

介绍关于TemplateRef,ViewContainerRef

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值