Angular ElementRef,ViewContainerRef,TemplateRef 之一网打尽

前言

从一段代码引入今天的话题:

你见过下面的代码吗?

js 复制代码
document.getElementById();
document.getElementsByClassName();
document.createElement();

相信学习过前端基础的伙伴们对这句话并不陌生,或者用过jQuery的小伙伴都知道,这类代码都是在现在主流框架出来前,前端工程师们每天都要打交道的工作--对Dom进行增删改查 ,从而实现用户所看到的交互效果。 那么当我们拥有了强大功能的Angular框架以后,是否还有这样的一个途径让我们直接接触到原生DOM,甚至对它进行操作呢?

答案是肯定的~让我们把目光聚焦到Angular提供的这几个对象ElementRefViewContainerRefTemplateRef

ElementRefViewContainerRefTemplateRef的概念

从官网文档来看:
ElementRef : 原生元素的包装器。也就是说它在原生dom外面又包裹了一层,其中nativeElement属性中就是我们上面代码获取的DOM对象。
ViewContainerRef :视图容器,可以将一至多个视图附着其中。它有个属性element,类型为ElementRef,但是该属性里面包含的nativeElment只是一个锚点,<!--ng-container-->用来标记container在HTML存在的位置。
TemplateRef :内嵌模板,可以通过ViewContainerRefcreateEmbeddedView方法去实例并且放置到视图容器中,它同样具有elementRef这个对象,类型为ElementRef,也是存的锚点位置<!--container-->

下面就跟着我从代码的角度去理解这三个对象,和他们之间的关系吧~ 首先,让我们一起新建一个新的Angular项目,执行下面的命令行(我目前的angular版本为14,在命令中默认使用css样式,关闭ssr和standalone模式,目的是向低版本angular兼容,低版本可以执行ng new my-element-explore):

ng new my-element-explore --style=css --standalone=false --ssr=false

清除app.component.html的内容,并且添加以下内容:

html 复制代码
<h1 id="domElement" #domId>H1: This is a Dom Element</h1>

<ng-container #containerId>
    <h2 id="containerElement">H2: This is a content in ng-container</h2>
</ng-container>

<ng-template #templateId>
  <h3 id="templateElement">H3: This is a content in ng-template</h3>
</ng-template>

替代app.component.ts内容如下:

js 复制代码
import { AfterViewInit, Component, ViewChild, ViewContainerRef } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit{
  
  title = 'my-element-explore';
  @ViewChild('domId') dom;
  @ViewChild('containerId', {read: ViewContainerRef}) viewContainer: ViewContainerRef;
  @ViewChild('templateId') template;
  

  ngAfterViewInit(): void {
    console.log('dom', this.dom);
    console.log('viewContainer', this.viewContainer);
    console.log('template', this.template);
  }
}

启动项目ng serve,并且前往localhost:4200看下效果和控制台输出:

首先我们简单解释下上面的代码:Angular中有内置标签<ng-template>表示一个模板片段,如果你不告诉angular解析器你想要放在在何处,那它就不会在浏览器中显示出来。(从我们的例子中你也可以看出,页面上只能看到h1h2两段文字)。<ng-container>则表示一个容器,他并没有实际对应的dom对象,当渲染完成的时候,只有被他包裹的内容会出现在页面中,如果你用developer tool去看的话,html里面只有<h2>标签。针对Angular Html模板中的所有元素(标签),我们都可以用#name去标记它,然后在ts文件中再通过@viewChild('name')去获得这个对象(其中我们用到read这个参数,后面会详细讲解 ),从我们输出的结果中可以看出,页面上的三组对象分别是ElementRefViewContainerRefTemplateRef

随后我们验证一下dom中的nativeElement属性是不是原生DOM元素,在ngAfterViewInit里面加上下面代码:

js 复制代码
console.log('nativeElment', this.dom.nativeElement === document.getElementById('domElement'))

会在控制台中看到 true,因而我们可以确认ElementRef中的nativeElement就是原生DOM对象。

如何操作ElementRef

那么获得了DOM, 下面就要探究如何改变dom的形态,甚至对它进行增删呢?

抱歉的通知你,Angular并不推荐你直接获取nativeElement并且对其操作,官方给出的原因是这样会增加XSS攻击的风险。不过他提供了一个新的API供你操作:Renderer2。里面你会发现很多跟document类似的方法,参考官方文档。

写个例子练下手,在app.component.ts中粘贴如下代码

js 复制代码
import { AfterViewInit, Component, Renderer2, ViewChild, ViewContainerRef } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit{
  
  title = 'my-element-explore';
  @ViewChild('domId') dom;
  @ViewChild('containerId', {read: ViewContainerRef}) viewContainer: ViewContainerRef;
  @ViewChild('templateId') template;

  constructor(private renderer2: Renderer2){}
  ngAfterViewInit(): void {
    this.renderer2.setStyle(this.dom.nativeElement, 'color', 'red');
  }
}

运行结果如下:

由此可见,我们已经成功改变了第一行元素的样式~在Angular中对DOM元素的操作就算完成啦,更多详情请仔细阅读Renderer2文档哦!

ViewContainerRefTemplateRef 配合使用

让我们聚焦到上面提到的ng-template,该元素内部是一个h3标签,但是我们在页面上并没有看见它,源自于ng-template它在未指定显示在哪个容器前是不会出现在浏览器页面中。那么如果在ts文件中给他指定容器,那就需要用到我们的ViewContainerRef中的createEmbeddedView()方法啦。 试着在ngAfterViewInit中添加下面的代码:

js 复制代码
this.viewContainer.createEmbeddedView(this.template);

也就是说咱们把ng-template里的内容放置到ViewContainerRef对应的ng-container里面,页面上就会出现下面的h3内容啦:

可以看到createEmbeddedView方法是将TemplateRef对象插入视图的最末端,并不影响其他元素的位置,如果你想移除掉视图里的内嵌内容(我理解为通过createEmbeddedView插入的内容),可以先调用ViewContainerRefclean()方法清空他们。

ViewContainerRefElementRef

Component层面,一个Component里面会各有一个ViewContainerRefElementRef对象,我们可以直接在constructor()构造器中直接实例化他们,即可获得相应的对象:

ts 复制代码
import { Component, ElementRef, OnInit, ViewContainerRef } from '@angular/core';
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit{
    constructor(private vcComponentRef: ViewContainerRef, private eleComponentRef: ElementRef){}
    ngOnInit(): void {
        console.log(this.vcComponentRef);
        console.log(this.eleComponentRef);
      }
}

从控制台看输出内容:

由此可见他们都是指向<app-root>标签的,那试问以下代码会返回什么值呢?

js 复制代码
 console.log(this.vcComponentRef.element === this.eleComponentRef); // false -- 看来ViewContainerRef.element和elementRef并不能完全划等号
 console.log(this.vcComponentRef.element.nativeElement === this.eleComponentRef.nativeElement); // true -- 但是他们内部存储的原生DOM确实是同一个

同时由于自身component就有视图对象,因此我们也可以把ng-template中的内容嵌入到Component对应的视图中,在保持上述html不变的前提下,执行以下代码:

js 复制代码
import { AfterViewInit, Component, ElementRef, ViewChild, ViewContainerRef } from '@angular/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit{
  
  title = 'my-element-explore';
  @ViewChild('domId') dom: any;           // 确认该对象前,不指定类型
  @ViewChild('containerId', { read: ViewContainerRef })
  viewContainer!: any;                    // 默认是elementRef对象,因此必须指定read
  @ViewChild('templateId') template: any; // 确认该对象前,不指定类型
  constructor(private vcComponentRef: ViewContainerRef, private tempComponent: ElementRef){}

  ngAfterViewInit(): void {
    this.viewContainer.createEmbeddedView(this.template);
    this.vcComponentRef.createEmbeddedView(this.template);
  }
}

渲染结果:

由此可见ng-template内容被插入了两次 ,一次是插入我们html中定义的ng-container,另一个则是插入了component层面的视图中。

后话

其实针对ng-templateng-containerng-content里面还有很多知识,不过由于今天的主题是各种Ref的关系,因此就在这里小小挖个坑,日后再填啦。

下面一篇文章会先记录下在使用@viewChild的时候一些领悟,敬请期待一下~

这是本章内容的示例代码,按需取用~(VickySH9112020/my-element-explore (github.com))。

相关推荐
界面开发小八哥10 天前
界面控件Kendo UI for Angular中文教程:如何构建带图表的仪表板?(三)
前端·ui·界面控件·kendo ui·angular.js·ui开发
布兰妮甜11 天前
Angular模块化应用构建详解
javascript·angular.js·模块化
前端郭德纲18 天前
什么是Angular?
javascript·angular.js
JerryXZR23 天前
Angular面试题汇总系列一
前端·javascript·angular.js
余生H1 个月前
Angular v19 (二):响应式当红实现signal的详细介绍:它擅长做什么、不能做什么?以及与vue、svelte、react等框架的响应式实现对比
前端·vue.js·react.js·angular.js
知野小兔1 个月前
【Angular】async详解
前端·javascript·angular.js
知野小兔1 个月前
【Angular】eventDispatcher详解
前端·javascript·angular.js
爱学习的小康1 个月前
使用pdfmake导出pdf文件
javascript·node.js·angular.js
界面开发小八哥1 个月前
界面控件Kendo UI for Angular中文教程:如何构建带图表的仪表板?(一)
ui·界面控件·kendo ui·angular.js·ui开发
布兰妮甜1 个月前
前端框架大比拼:React.js, Vue.js 及 Angular 的优势与适用场景探讨
前端·vue.js·react.js·前端框架·angular.js