Angular 视图引用

前言

从简单说起,与视图相关的几个对象实例:TemplateRef、ViewContainerRef、ElementRef,不管是哪一个,它们共同的说点就是后面都带上了 Ref,我们可以理解成 xx 的引用。我们可以通过这些对象引用来对视图进行操作(向指定位置添加 html 内容或者组件)。

不管是 TemplateRef、ViewContainerRef 还是 ElementRef 都可以配合 @ViewChild() 方法来使用,即通过 @ViewChild() 来查询元素并使用 TemplateRef、ViewContainerRef 或者 ElementRef 来声明需要返回的类型。

其实我们大致可以这么理解

  • TemplateRef:用于模板
  • ViewContainerRef:用于容器
  • ElementRef:用于元素

@ViewChild() 中的第一个参数就是一个模板变量,@ViewChild() 会根据这个变量在 html 元素中查找,比如我们获取一个名为 tpl 并返回 TemplateRef 引用的对象

<!-- template-demo.component.html -->
  <!-- 模板 -->
  <ng-template #tpl>
    <div class="message">这里是要显示的内容</div>
  </ng-template>
  <!-- 容器 -->
  <ng-container #container></ng-container>
  <button (click)="createContent()">添加模板内容到容器</button>
  <button (click)="clearContainer()">清空容器</button>

template-demo.component.ts

import { Component, ViewChild, ViewContainerRef, TemplateRef } from '@angular/core';
@Component({
  selector: 'template-demo',
  templateUrl: './template-demo.component.html',
  styleUrls: ['./template-demo.component.less']
})
export class TemplateDemoComponent{
  constructor(){}
  // 参数 { read: ViewContainerRef } 定义查询的类型,这里的 ViewContainerRef 表示一个容器类型,但它不能被 Angular 推断,所以如果是被查询的元素是一个容器类型,那么必需定义,
  // 其它类型则可以不声明,Angular 会自动推断
  @ViewChild("container", { read: ViewContainerRef }) containerRef: ViewContainerRef; // 获取容器引用
  @ViewChild("tpl") tplRef: TemplateRef<any>; // 获取模板引用
  createContent(){
    this.containerRef.createEmbeddedView(this.tplRef) // 把模板内容放到容器中进行显示
  }
  clearContainer () { 
    // 清空容器
    this.containerRef.clear()
  }
}

不过上面的代码我们还可以进行简化,只用一个 <ng-template> 标签实现

<!-- template-demo.component.html -->
<ng-template #tpl>
  <div class="message">这里是要显示的内容</div>
</ng-template>
......

删掉 <ng-container> 标签,只保留 <ng-template> 标签,接着修改下 .ts 文件

// template-demo.component.ts
......
@ViewChild("tpl", { read: ViewContainerRef }) containerRef: ViewContainerRef; // 获取容器引用
@ViewChild("tpl") tplRef: TemplateRef<any>; // 获取模板引用
......

把 <ng-template> 标签作为模板,同时也作为容器来使用。

需要注意的是 <ng-template> 和 <ng-container> 元素会被替换成 comment 元素(<!-- -->)。任意 html 元素都可用作容器。

ElementRef 用来操作 DOM 元素,比如:给设置背景色,字体颜色等。

<!-- template-demo.component.html -->
<div #ele>我只是一个元素</div>

这里我们简单地添加一个元素,并给上模板变量。

// template-demo.component.ts
import { Component, ViewChild, ElementRef, AfterViewInit, Renderer2 } from '@angular/core';
@Component({
  selector: 'template-demo',
  templateUrl: './template-demo.component.html',
  styleUrls: ['./template-demo.component.less']
})
export class TemplateDemoComponent implements AfterViewInit{
  constructor(private renderer: Renderer2){}
  @ViewChild("ele") elRef: ElementRef; // 获取元素引用
  ngAfterViewInit(){
    this.renderer.setStyle(this.elRef.nativeElement, "color", "red")
  }
}

要想更好的使用 Renderer2 我们必需得知道有可以为我们做些什么,即他有什么方法可用:

// 实现此回调以便销毁渲染器或其宿主元素
abstract destroy(): void;
// 实现此回调以便创建宿主元素的实例
abstract createElement(name: string, namespace?: string | null): any;
// 实现此回调以便向宿主元素的 DOM 中添加一个注释
abstract createComment(value: string): any;
// 实现此回调以便向宿主元素的 DOM 中添加文本
abstract createText(value: string): any;
// 把子元素追加到宿主元素 DOM 中的指定父节点下。
abstract appendChild(parent: any, newChild: any): void;
// 实现此回调,以便往宿主元素中父节点的指定位置插入一个子节点
abstract insertBefore(parent: any, newChild: any, refChild: any): void;
// 实现此回调以便从宿主元素的 DOM 中移除一个子节点
abstract removeChild(parent: any, oldChild: any): void;
// 实现此回调以准备将其作为根元素进行引导的元素,返回该元素的实例
abstract selectRootElement(selectorOrNode: string | any, preserveContent?: boolean): any;
// 实现此回调以获得宿主元素的 DOM 中指定节点的父节点
abstract parentNode(node: any): any;
// 实现此回调,以获得宿主元素的 DOM 中指定节点的下一个兄弟节点
abstract nextSibling(node: any): any;
// 实现此回调以便在 DOM 中设置指定元素的属性值
abstract setAttribute(el: any, name: string, value: string, namespace?: string | null): void;
// 实现此回调以便从 DOM 中某个元素上移除一个属性
abstract removeAttribute(el: any, name: string, namespace?: string | null): void;
// 实现此回调,以便为 DOM 中的某个元素添加一个 CSS 类
abstract addClass(el: any, name: string): void;
// 实现此回调,以便从 DOM 中的某个元素上移除一个 CSS 类
abstract removeClass(el: any, name: string): void;
// 实现此回调函数,以便为 DOM 中的某个元素设置 CSS 样式
abstract setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2): void;
// 实现此回调,以便从 DOM 中某个元素上移除一个 CSS 样式值
abstract removeStyle(el: any, style: string, flags?: RendererStyleFlags2): void;
// 实现此回调,以便设置 DOM 中某个元素的属性值
abstract setProperty(el: any, name: string, value: any): void;
// 实现本回调,以便在宿主元素中设置节点的值
abstract setValue(node: any, value: string): void;
// 实现此回调以启动事件监听器
abstract listen(target: 'window' | 'document' | 'body' | any, eventName: string, callback: (event: any) => boolean | void): () => void;

更详细的说明可以自己查阅源码。Renderer 和 Renderer2 是什么关系呢?

Renderer2 其实就是更新换代的产物,即相关 DOM 渲染操作我们用 Renderer2 来替代即将废弃的 Renderer。