Angular 2 TemplateRef & ViewContainerRef

阅读 Angular 6/RxJS 最新教程,请访问 前端修仙之路

TemplateRef

在介绍 TemplateRef 前,咱们先来了解一下 HTML 模板元素 - <template> 。模板元素是一种机制,容许包含加载页面时不渲染,但又能够随后经过 JavaScript 进行实例化的客户端内容。咱们能够将模板视做为存储在页面上稍后使用的一小段内容。javascript

在 HTML5 标准引入 template 模板元素以前,咱们都是使用 <script> 标签进行客户端模板的定义,具体以下:html

<script id="tpl-mock" type="text/template">
   <span>I am span in mock template</span>
</script>

对于支持 HTML5 template 模板元素的浏览器,咱们能够这样建立客户端模板:前端

<template id="tpl">
    <span>I am span in template</span>
</template>

下面咱们来看一下 HTML5 template 模板元素的使用示例:java

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"> <title>HTML5 Template Element Demo</title></head>
<body>
<h4>HTML5 Template Element Demo</h4>
<!-- Template Container -->
<div class="tpl-container"></div>
<!-- Template -->
<template id="tpl">
    <span>I am span in template</span>
</template>
<!-- Script -->
<script type="text/javascript">
    (function renderTpl() {
        if ('content' in document.createElement('template')) {
            var tpl = document.querySelector('#tpl');
            var tplContainer = document.querySelector('.tpl-container');
            var tplNode = document.importNode(tpl.content, true);
            tplContainer.appendChild(tplNode); 
        } else {
            throw  new Error("Current browser doesn't support template element");
        }
    })();
</script>
</body>
</html>

以上代码运行后,在浏览器中咱们会看到如下内容:node

HTML5 Template Element Demo

I am span in template

而当咱们注释掉 tplContainer.appendChild(tplNode) 语句时,刷新浏览器后看到的是:segmentfault

HTML5 Template Element Demo

这说明页面中 <template> 模板元素中的内容,若是没有进行处理对用户来讲是不可见的。Angular 2 中,<template> 模板元素主要应用在结构指令中,此外在 Angular 2 属性指令 vs 结构指令 文章中咱们也介绍了 <template> 模板元素和自定义结构指令,接下来咱们先来介绍一下本文中的第一个主角 - TemplateRef:浏览器

import {Component, TemplateRef, ViewChild, AfterViewInit} from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <template #tpl>
      <span>I am span in template</span>
    </template>
  `,
})
export class AppComponent {
  name: string = 'Semlinker';

  @ViewChild('tpl')
  tpl: TemplateRef<any>;

  ngAfterViewInit() {
    console.dir(this.tpl);
  }
}

上述代码运行后的控制台的输出结果以下:app

图片描述

从上图中,咱们发现 @Component template 中定义的 <template> 模板元素,渲染后被替换成 comment 元素,其内容为 "template bindings={}" 。此外咱们经过 @ViewChild 获取的模板元素,是 TemplateRef_ 类的实例,接下来咱们来研究一下 TemplateRef_ 类:this

TemplateRef_spa

// @angular/core/src/linker/template_ref.d.ts
export declare class TemplateRef_<C> extends TemplateRef<C> {
    private _parentView;
    private _nodeIndex;
    private _nativeElement;
    constructor(_parentView: AppView<any>, _nodeIndex: number, _nativeElement: any);
    createEmbeddedView(context: C): EmbeddedViewRef<C>;
    elementRef: ElementRef;
}

TemplateRef

// @angular/core/src/linker/template_ref.d.ts
// 用于表示内嵌的template模板,可以用于建立内嵌视图(Embedded Views)
export declare abstract class TemplateRef<C> {
    elementRef: ElementRef;
    abstract createEmbeddedView(context: C): EmbeddedViewRef<C>;
}

(备注:抽象类与普通类的区别是抽象类有包含抽象方法,不能直接实例化抽象类,只能实例化该抽象类的子类)

咱们已经知道 <template> 模板元素,渲染后被替换成 comment 元素,那么应该如何显示咱们模板中定义的内容呢 ?咱们注意到了 TemplateRef 抽象类中定义的 createEmbeddedView
抽象方法,该方法的返回值是 EmbeddedViewRef 对象。那好咱们立刻来试一下:

import {Component, TemplateRef, ViewChild, AfterViewInit} from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <template #tpl>
      <span>I am span in template</span>
    </template>
  `,
})
export class AppComponent {
  name: string = 'Semlinker';

  @ViewChild('tpl')
  tpl: TemplateRef<any>;

  ngAfterViewInit() {
    let embeddedView = this.tpl.createEmbeddedView(null);
    console.dir(embeddedView);
  }
}

上述代码运行后的控制台的输出结果以下:

图片描述

从图中咱们能够知道,当调用 createEmbeddedView 方法后返回了 ViewRef_ 视图对象。该视图对象的 rootNodes 属性包含了 <template> 模板中的内容。在上面的例子中,咱们知道了 TemplateRef 实例对象中的 elementRef 属性封装了咱们的 comment 元素,那么咱们能够经过 insertBefore 方法来建立咱们模板中定义的内容。

import { Component, TemplateRef, ViewChild, AfterViewInit } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <template #tpl>
      <span>I am span in template {{name}}</span>
    </template>
  `,
})
export class AppComponent {
  name: string = 'Semlinker';

  @ViewChild('tpl')
  tpl: TemplateRef<any>;

  ngAfterViewInit() {
    // 页面中的<!--template bindings={}-->元素
    let commentElement = this.tpl.elementRef.nativeElement;
    // 建立内嵌视图
    let embeddedView = this.tpl.createEmbeddedView(null);
    // 动态添加子节点
    embeddedView.rootNodes.forEach((node) => {
        commentElement.parentNode
          .insertBefore(node, commentElement.nextSibling);
    });
  }
}

成功运行上面的代码后,在浏览器中咱们会看到如下内容:

Welcome to Angular World

I am span in template

如今咱们来回顾一下,上面的处理步骤:

  • 建立内嵌视图(embedded view)
  • 遍历内嵌视图中的 rootNodes,动态的插入 node

虽然咱们已经成功的显示出 template 模板元素中的内容,但发现整个流程仍是太复杂了,那有没有简单地方式呢 ?是时候介绍本文中第二个主角 - ViewContainerRef。

ViewContainerRef

咱们先来检验一下它的能力,而后再来好好地分析它。具体示例以下:

import { Component, TemplateRef, ViewChild, ViewContainerRef, AfterViewInit } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <template #tpl>
      <span>I am span in template</span>
    </template>
  `,
})
export class AppComponent {
  name: string = 'Semlinker';

  @ViewChild('tpl')
  tplRef: TemplateRef<any>;

  @ViewChild('tpl', { read: ViewContainerRef })
  tplVcRef: ViewContainerRef;

  ngAfterViewInit() {
    // console.dir(this.tplVcRef); (1)
    this.tplVcRef.createEmbeddedView(this.tplRef);
  }
}

移除上面代码中的注释,便可在控制台看到如下的输出信息:

图片描述

而在浏览器中咱们会看到如下内容:

Welcome to Angular World

I am span in template

接下来咱们来看一下 ViewContainerRef_ 类:

// @angular/core/src/linker/view_container_ref.d.ts
// 用于表示一个视图容器,可添加一个或多个视图
export declare class ViewContainerRef_ implements ViewContainerRef {
    ...
    length: number; // 返回视图容器中已存在的视图个数
    element: ElementRef;
    injector: Injector;
    parentInjector: Injector;
      // 基于TemplateRef建立内嵌视图,并自动添加到视图容器中,可经过index设置
    // 视图添加的位置
    createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, 
      index?: number): EmbeddedViewRef<C>;
    // 基 ComponentFactory建立组件视图
    createComponent<C>(componentFactory: ComponentFactory<C>,
      index?: number, injector?: Injector, projectableNodes?: any[][]): ComponentRef<C>;
    insert(viewRef: ViewRef, index?: number): ViewRef;
    move(viewRef: ViewRef, currentIndex: number): ViewRef;
    indexOf(viewRef: ViewRef): number;
    remove(index?: number): void;
    detach(index?: number): ViewRef;
    clear(): void;
}

经过源码咱们能够知道经过 ViewContainerRef_ 实例,咱们能够方便地操做视图,也能够方便地基于 TemplateRef 建立视图。如今咱们来总结一下 TemplateRef 与 ViewContainerRef。

TemplateRef:用于表示内嵌的 template 模板元素,经过 TemplateRef 实例,咱们能够方便建立内嵌视图(Embedded Views),且能够轻松地访问到经过 ElementRef 封装后的 nativeElement。须要注意的是组件视图中的 template 模板元素,通过渲染后会被替换成 comment 元素。

ViewContainerRef:用于表示一个视图容器,可添加一个或多个视图。经过 ViewContainer
Ref 实例,咱们能够基于 TemplateRef 实例建立内嵌视图,并能指定内嵌视图的插入位置,也能够方便对视图容器中已有的视图进行管理。简而言之,ViewContainerRef 的主要做用是建立和管理内嵌视图或组件视图。

我有话说

1.Angular 2 支持的 View(视图) 类型有哪几种 ?

  • Embedded Views - Template 模板元素
  • Host Views - Component 组件

1.1 如何建立 Embedded View

ngAfterViewInit() {
    let view = this.tpl.createEmbeddedView(null);
}

1.2 如何建立 Host View

constructor(private injector: Injector,
    private r: ComponentFactoryResolver) {
    let factory = this.r.resolveComponentFactory(AppComponent);
    let componentRef = factory.create(injector);
    let view = componentRef.hostView;
}

2.Angular 2 Component 组件中定义的 <template> 模板元素为何渲染后会被移除 ?

由于 <template> 模板元素,已经被 Angular 2 解析并封装成 TemplateRef 实例,经过 TemplateRef 实例,咱们能够方便地建立内嵌视图(Embedded View),咱们不须要像开篇中的例子那样,手动操做 <template> 模板元素。

3.ViewRef 与 EmbeddedViewRef 之间有什么关系 ?

ViewRef 用于表示 Angular View(视图),视图是可视化的 UI 界面。EmbeddedViewRef 继承于 ViewRef,用于表示 <template> 模板元素中定义的 UI 元素。

ViewRef

// @angular/core/src/linker/view_ref.d.ts
export declare abstract class ViewRef {
    destroyed: boolean;
    abstract onDestroy(callback: Function): any;
}

EmbeddedViewRef

// @angular/core/src/linker/view_ref.d.ts
export declare abstract class EmbeddedViewRef<C> extends ViewRef {
    context: C;
    rootNodes: any[]; // 保存<template>模板中定义的元素
    abstract destroy(): void; // 用于销毁视图
}

总结

Angular 2 中 TemplateRef 与 ViewContainerRef 的概念对于初学者来讲会比较羞涩难懂,本文从基本的 HTML 5 <template> 模板元素开始,介绍了如何操做和应用页面中定义的模板。而后经过实例介绍了 Angular 2 中 TemplateRef 和 ViewContainerRef 的定义和做用。但愿经过这篇文章,读者能更好的理解 TemplateRef 与 ViewContainerRef。