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))。

相关推荐
欧阳天羲3 天前
Angular 框架下 AI 驱动的企业级大前端应用开
前端·人工智能·angular.js
甜瓜看代码7 天前
1.
react.js·node.js·angular.js
天若有情6738 天前
React、Vue、Angular的性能优化与源码解析概述
vue.js·react.js·angular.js
啃火龙果的兔子10 天前
Angular 从框架搭建到开发上线的完整流程
前端·javascript·angular.js
葡萄城技术团队12 天前
Angular V20 新特性
angular.js
hashiqimiya25 天前
AngularJS 待办事项 App
前端·javascript·angular.js
ze_juejin1 个月前
Subject、BehaviorSubject、ReplaySubject、AsyncSubject、VoidSubject比较
前端·angular.js
fanged1 个月前
Angular--Hello(TODO)
前端·javascript·angular.js
crary,记忆1 个月前
MFE微前端高级版:Angular + Module Federation + webpack + 路由(Route way)完整示例
前端·webpack·angular·angular.js
ze_juejin1 个月前
Angular NgZone 详解
angular.js