angular 模板引用变量 ElementRef 渲染器

模板引用变量通常用来引用模板中的某个Dom元素。它还可以引用angular组件或指令或WebComponent。模板引用变量可以让我们在页面上声明变量并使用,也是一个可以让ts控制html的桥梁。

使用井号#来声明引用变量,也可以使用ref-前缀来代替#

1
2
<input type="text" #code/>
<button (click)="consoleValue(code)">submit</button>

我们同时写了个方法将模板引用变量输出看看是什么东西:

1
2
3
consoleValue(value: any) {
console.log(value);
}

运行发现,这个模板引用变量就是一个Dom元素(这不是废话么!)。

根据模板引用变量对应的Dom元素,它可以有不同的行为和属性。我们这里引用的事一个input元素,所以我们可以通过code.value来查看输入的内容。

模板引用变量的作用范围是整个模板,所以不要在同一个模板里面多次定义同一个变量名,否则它在运行期间的值是无法确定的。

如何在ts中使用模板引用变量?

我们需要用到ViewChild。使用ViewChild可以声明一个视图查询,变更检测器会在视图的DOM中查找能匹配上该选择器的第一个元素或指令。如果视图的DOM发生了变化,出现了匹配该选择器的子节点,该属性会被更新。支持的选择器包括:

  • 任何带有@Component@Directive装饰器的类,比如可以使用@ViewChild(Pane)来查询声明的指令export class Pane
  • 字符串形式的模板引用变量,比如可以使用@ViewChild('cmp')来查询<my-component #cmp></my-component>
  • 组件树种任何当前子组件所定义的提供商
  • 任何通过字符串令牌定义的提供商
  • TemplateRef,比如可以使用@ViewChild(TemplateRef) template来查询<ng-template></ng-template>

在这里我们是使用ViewChild查询一个模板引用变量,所以:

1
@ViewChild('code') codeEl: ElementRef;

这里我们的模板引用变量是声明在一个DOM元素上,所以我们给它的类型是ElementRefElementRef是声明了一原生元素的包装器,允许直接访问DOM元素。

注意:当需要访问DOM元素时,这个API作为最后的选择,优先使用Angular提供的模板和数据绑定机制。或者可以使用Render2,它提供了可安全使用的API(即使环境没有提供直接访问原生元素的功能)。

其他的先不管,我们可以使用ElementRef来操作DOM的属性。比如设置这个input的value:

1
this.codeEl.nativeElement.value = 'set value';

运行后我们可以看到在页面上,set value这个字符串会显示在input输入框里面。

使用渲染器操作DOM

渲染器是angular的一种内置服务,用于执行UI渲染。在浏览器中,渲染是将模型映射到视图的过程,也就是「从JavaScript中的原始数据或其他数据对象」–TS 到「页面中的段落、表单、按钮等其他页面元素」–DOM。

在实例化一个组件时,Angular会调用renderComponent()方法并将其获取的渲染器与该组件实例相关联,angular会在渲染组件时通过渲染器执行相应的操作,比如:创建元素、设置属性、添加样式和订阅事件等。

值得注意的是,在Angular4.x+版本中,我们使用Renderer2来替代Renderer,通过观察相关抽象类可以发现,除了Render2定义了更多的抽象方法之外,在一些主要方法上新增了namespace这个参数(namespace作用于xml中,html中基本没用)。

值得注意的是,尽管可以不用渲染器也可以使用document来创造元素,但是使用document创建的元素不具备_ngcontent这个属性,而这个属性恰恰会标记处该元素是属于按个宿主的,所以使用document创建的元素无法应用当前组件的css样式,而使用renderer2渲染器创建的Dom会自动附加这个_ngcontent属性。

引入Renderer2:

1
2
3
4
constructor(
private render: Renderer2,
private el: ElementRef,
) {}

获得当前组件的宿主元素

我们接下来会进行一些元素的添加或删除等操作,这前提就是我们需要获得当前组件的宿主元素,要不然也无法新增元素。那怎么获取当前组件的宿主元素呢?

我们可以在构造函数中注入ElementRef,这样声明的这个ElementRef就是我们当前的组件的宿主元素:

1
2
3
4
5
6
7
constructor(
private render: Renderer2,
private el: ElementRef,
) {}
ngAfterViewInit() {
console.log(this.el.nativeElement);
}

上面我们在当前组件的构造函数中注入了ElementRef,出于考虑安全问题,我们将dom操作都放在ngAfterViewInit生命周期里面,输出我们的el可看到输出的是当前组件的宿主元素。

创建元素 createElement

使用Renderer2createElement方法来创建元素,语法:

1
createElement(name: string, namespace?: string): any

创建一个宿主元素,name为新元素的标识名,在指定的命名空间内应该是唯一的;namespace表示新元素的命名空间。

例如,创建一个input元素:

1
2
const dom = this.render.createElement('input');
console.log(this.el.nativeElement);

可以看到,输出的是带当前组件层级_ngcontent的input元素,

同样的创建函数有:

  • createCommont(value: string) 添加一个注释的DOM
  • createText(value: string) 添加一个文本的DOM

上面的三个创建DOM的方法只是被创建了DOM而已,还没有被渲染到视图界面上,也就是页面上根本不存在这个新创建的元素。还需要通过appendChild或者insertBefor方法来添加到视图中去。

将DOM添加到视图中 appendChild

通过appendChild可以将一个创建的元素追加到一个父元素下面,语法:

1
appendChild(parent: any, newChild: any): void

比如,我们要在当前组件中添加上面我们创建的input元素:

1
2
const dom = this.render.createElement('input');
this.render.appendChild(this.el.nativeElement, dom);

这样可以在页面上看到确实是有了一个input元素。前面说过我们通过注入ElementRef获取到了当前组件的宿主元素,然后通过appendChild方法向当前组件中添加了一个input元素。

同样的方法有insertBefor,语法为:

1
2
3
4
5
6
7
8
insertBefore(parent: any, newChild: any, refChild: any): void
```
在宿主元素中父节点的指定位置之前插入新元素,例如:
``` js
const dom = this.render.createElement('input');
this.render.appendChild(this.el.nativeElement, dom);
const testEl = this.render.createText('test');
this.render.insertBefore(this.el.nativeElement, testEl, dom);

文本元素将会被插入在input元素之前。

选择器方法

不仅仅是创建元素,我们还需要获取DOM中的元素。

  • parentNode(node: any): any,用来获取宿主元素中的DOM中指定元素的父节点,如果没有则为null。
  • nextSibling(node: any): any,用来获取宿主元素中的DOM中指定元素的下一个兄弟节点,如果没有则为null。
  • selectRootElement(selectorOrNode: any) 返回将其作为根元素进行引导的元素。

设置/移除元素属性

使用setAttribute()方法可以设置指定元素的属性。语法:setAttribute(el: any, name: string, value: string, namespace?: string): void

  • el 目标元素
  • name 属性名称
  • value 属性值

使用removeAttribute()方法可以从某个元素上移除某个属性,语法:removeAttribute(el: any, name: string, namespace?: string): void

  • el 目标元素
  • name 属性名称
1
2
3
4
5
const p = this.render.createElement('p');
this.render.setAttribute(p, 'id', 'test');
this.render.setProperty(p, 'className', 'test');
this.render.setProperty(p, 'innerText', 'test');
this.render.appendChild(this.el.nativeElement, p);

上面代码添加了一个p元素,然后给这个p元素添加了id、class和里面的文字。

当然,也对应着移除属性的方法:removeAttribute(el: any, name: string, namespace?: string): void。例如,移除上面添加的id;

1
2
const p = document.getElementById('test');
this.render.removeAttribute(p, 'id');

设置样式

  • 添加类:addClass(el: any, name: string): void
  • 移除类:removeClass(el: any, name: string): void
  • 设置样式:setStyle(el: any, name: string, value: any, flags?): void

例如:

1
2
3
this.render.addClass(p, 'test2');
this.render.removeClass(p, 'test1');
this.render.setStyle(p, 'width', '300px');

给p元素添加test2类,并删除test1类,并修改样式width为300px。

移除元素

方法removeChild(parent: any, oldChild: any): void可以从父节点中移除子节点。
比如,移除我们添加的那个p元素:

1
this.render.removeChild(this.el.nativeElement, p);

设置事件监听

使用listen函数可以给元素设置监听事件:

1
listen(target: any, eventName: string, callback: (event: any) => boolean | void): () => void

参数:

  • target 要监听事件的上下文,可以是整个窗口或文档,也可以是body或指定的DOM元素。
  • eventName 要监听的事件
  • callback 当事件发生时的处理回调。

返回一个取消监听的函数,用于解除该监听事件。

模板输入变量

模板输入变量是可以在单个实例的模板中引用值的变量。可以使用let关键字在模板中声明一个输入变量。这个变量的范围被限制在所重复的模板的单一实例上。可以在其他结构型指令上使用同样的变量名,它们互不影响。例如:

1
<p *ngFor="let item of [1, 2, 3]">{{item}}</p>

这里let item就是声明了一个模板输入变量,迭代数组的值。


写了一大堆,就着文档来总结,归根结底还需要在真正使用的时候再完善一些用法和没注意的地方。

码字辛苦,打赏个咖啡☕️可好?💘