angular - 常用类和属性
angular 不要使用window
、 document
、 navigator
等浏览器特有的类型以及直接操作DOM元素。
这样就引出了 Angular 主要特性之一:横跨所有平台。通过合适的方法,使用 Angular 构建的应用,可复用在多种不同平台的应用上 —— Web、移动 Web、移动应用、原生应用和桌面原生应用。
为了能够支持跨平台,Angular 通过抽象层封装了不同平台的差异。比如定义了抽象类 Renderer2 、抽象类 RootRenderer 等。此外还定义了以下引用类型:ElementRef、TemplateRef、ViewRef 、ComponentRef 和 ViewContainerRef 等。通过 模板变量
、@ViewChild
等方法获取DOM元素。
Dom 相关
为了演示,先定义一个组件DemoComponent:
import { AfterViewInit, Component, ElementRef, Renderer2, ViewChild } from '@angular/core';
@Component({
template: `
<div id="demoDiv" #demoDiv>this is DIV!</div>
DIV的id:{{demoDiv.id}} <!-- DIV的id:demoDiv -->
`
})
export class DemoComponent implements AfterViewInit {
@ViewChild('demoDiv') demoDiv: ElementRef; // @ViewChild通过模板变量名获取
constructor(private renderer: Renderer2) {
}
ngAfterViewInit() {
console.log('DIV的id:' + this.demoDiv.nativeElement.id); // DIV的id:demoDiv
this.renderer.setStyle(this.demoDiv.nativeElement, 'background-color', 'red'); // 通过Renderer2设置div的css样式background-color
}
}
获取组件中的div
在Angular应用中不应该通过原生 API 或者 jQuery 来直接获取DOM元素:
let element1 = document.getElementById("demoDiv"); // jQuery获取: $('#demoDiv'),angular 不应该使用
而是应该通过Angular提供的方法来获取DOM元素:
<div id="demoDiv" #demoDiv>this is DIV!</div>
DIV的id:{{demoDiv.id}} <!-- DIV的id:demoDiv -->
在组件模板中,我们在 div 上定义了 #demoDiv 的模板变量,那么 demoDiv 就等于该 div 的 DOM 对象,因此我们可以通过 demoDiv.id
直接获取 div 的 id。
@ViewChild 单元素
@ViewChild('demoDiv') demoDiv: ElementRef; // @ViewChild通过模板变量名获取
ngAfterViewInit() {
console.log('DIV的id:' + this.demoDiv.nativeElement.id); // DIV的id:demoDiv
}
在组件类中,我们通过 @ViewChild 获取到包装有 div 的 DOM 对象的 ElementRef 对象
class ElementRef<T> {
constructor(nativeElement: T)
nativeElement: T
}
因此我们可以在 ngAfterViewInit 中通过 this.demoDiv.nativeElement 获取到该 div 的 DOM 对象,然后获取元素的id。
@ViewChildren 一组元素
ViewChildren
是 Angular 中的一个装饰器,它可以用来获取视图中的子元素。这些子元素可以是 DOM 元素,也可以是 Angular 组件或指令。ViewChildren
会返回一个 QueryList
,这是一个包含了所有匹配元素的列表。
ViewChildren
的使用方法如下:
例子1:获取所有的 DOM 元素 myDiv
import { Component, ViewChildren, QueryList, ElementRef, AfterViewInit } from '@angular/core';
@Component({
selector: 'app-parent',
template: `
<div #myDiv>Div 1</div>
<div #myDiv>Div 2</div>
<div #myDiv>Div 3</div>
`,
})
export class ParentComponent implements AfterViewInit {
@ViewChildren('myDiv') divs: QueryList<ElementRef>;
ngAfterViewInit() {
console.log('div的数量:', this.divs.length);
this.divs.forEach(div => console.log(div.nativeElement.textContent));
}
}
例子2:获取所有的子组件
import { Component, ViewChildren, QueryList, AfterViewInit } from '@angular/core';
import { ChildComponent } from './child.component';
@Component({
selector: 'app-parent',
template: `
<app-child></app-child>
<app-child></app-child>
<app-child></app-child>
`,
})
export class ParentComponent implements AfterViewInit {
@ViewChildren(ChildComponent) children: QueryList<ChildComponent>;
ngAfterViewInit() {
console.log('子组件的数量:', this.children.length);
this.children.forEach(child => console.log(child));
}
}
ElementRef
在实际应用中获取视图层的dom元素,借助Angular提供的依赖注入机制,轻松访问到dom元素
ElementRef的主要作用是在Angular的组件中获取对DOM元素的引用,以便在组件中操作DOM元素。通过ElementRef,可以在组件中调用DOM API来改变元素的属性、样式或内容等。
在ElementRef的定义中,有一个nativeElement
属性,它是一个原生DOM元素。在应用中,我们通常会将ElementRef实例注入到组件的构造函数中,然后通过nativeElement
属性来访问DOM元素。
export class AppComponent {
constructor(private elementRef: ElementRef) {
const myAppElement = this.elementRef.nativeElement;
myAppElement.style.backgroundColor = 'red';
}
}
一般引用在 ngAfterViewInit
,
这是因为在ngAfterViewInit()生命周期钩子方法执行时,Angular已经完成了组件视图的初始化,此时才能确保DOM元素已经渲染完成并可以使用。
在ngAfterViewInit()生命周期钩子方法中使用ElementRef,可以确保我们获取到的DOM元素是已经初始化完成的,从而可以对其进行操作,比如修改样式、内容等。
同时,建议在ngOnDestroy()生命周期钩子方法中清理对DOM元素的引用,以防止内存泄漏
import { Component, ElementRef, AfterViewInit, OnDestroy } from '@angular/core';
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterViewInit, OnDestroy {
constructor(private elementRef: ElementRef) { }
ngAfterViewInit() {
const myAppElement = this.elementRef.nativeElement;
myAppElement.style.backgroundColor = 'red';
}
ngOnDestroy() {
const myAppElement = this.elementRef.nativeElement;
myAppElement = null;
}
}
QueryList
QueryList是一种特殊的列表,它存储着项目列表对象。这些对象可以被用来查询DOM元素或指令。
QueryList可以用于@ViewChildren
和@ContentChildren
装饰器中。@ViewChildren
装饰器用来查找在视图中定义的DOM元素或指令,而@ContentChildren
装饰器则用来查找在内容投影中定义的DOM元素或指令。
QueryList具有以下特点:
- 当应用程序的状态发生变化时,Angular会自动为QueryList更新对象项。
- QueryList中的每个对象都是一个项目列表对象,可以使用first、last、length、map、filter、find、reduce等方法进行操作。
- 可以通过toArray()方法将QueryList转换为数组形式。
- 可以使用changes()方法订阅QueryList的变化,返回一个Observable。
可以通过订阅 changes
Observable 来观察变化。
以下是 QueryList
的一些方法:
get(index: number): T | undefined
:返回索引处的QueryList
条目。map<U>(fn: (item: T, index: number, array: T[]) => U): U[]
:参见Array.map
。filter(fn: (item: T, index: number, array: T[]) => boolean): T[]
:参见Array.filter
。find(fn: (item: T, index: number, array: T[]) => boolean): T | undefined
:参见Array.find
。reduce<U>(fn: (prevValue: U, curValue: T, curIndex: number, array: T[]) => U, init: U): U
:参见Array.reduce
。forEach(fn: (item: T, index: number, array: T[]) => void): void
:参见Array.forEach
。
操作组件中的div
在上面通过几种方式获取到 div 的 DOM 对象,那么我们要如果对它进行操作呢(设置样式、属性、插入子元素等)?通过原始API 或者 jQuery 肯定是不允许的了。
这样我们就引出Angular抽象类 Renderer2
来对元素进行设置样式、属性、插入子元素等操作。
Renderer2 的定义如下:
class Renderer2 {
get data: {...}
destroyNode: ((node: any) => void) | null
destroy(): void
createElement(name: string, namespace?: string | null): any // 创建元素
createComment(value: string): any // 创建注释元素
createText(value: string): any // 创建文本元素
appendChild(parent: any, newChild: any): void // 添加子元素(在最后)
insertBefore(parent: any, newChild: any, refChild: any): void // 添加子元素(在最前)
removeChild(parent: any, oldChild: any): void // 移除子元素
selectRootElement(selectorOrNode: string | any): any // 获取根元素
parentNode(node: any): any // 获取父元素
nextSibling(node: any): any // 获取下一个兄弟元素
setAttribute(el: any, name: string, value: string, namespace?: string | null): void // 设置属性
removeAttribute(el: any, name: string, namespace?: string | null): void // 移除属性
addClass(el: any, name: string): void // 添加样式类
removeClass(el: any, name: string): void // 移除样式类
setStyle(el: any, style: string, value: any, flags?: RendererStyleFlags2): void // 设置样式
removeStyle(el: any, style: string, flags?: RendererStyleFlags2): void // 移除样式
setProperty(el: any, name: string, value: any): void // 设置DOM对象属性,不同于元素属性
setValue(node: any, value: string): void // 设置元素值
listen(target: 'window' | 'document' | 'body' | any, eventName: string, callback: (event: any) => boolean | void): () => void // 注册事件
}
因此,我们想改变 div 的背景色,就可以这样做:
ngAfterViewInit() {
this.renderer.setStyle(this.demoDiv.nativeElement, 'background-color', 'red'); // 通过Renderer2设置div的css样式background-color
}
Renderer2
Renderer2是Angular中的一个类,它提供了一种简单的方式来包装DOM操作和浏览器API。Renderer2是Angular的内部API,一般不直接在应用中使用,但在某些情况下,如编写自定义指令或内部Angular代码时,可能会用到。
Renderer2主要用来处理一些DOM操作,如创建元素、添加样式、删除样式、添加/删除类名、获取/设置属性等。以下是一些Renderer2的基础示例:
import { Renderer2, ElementRef } from '@angular/core';
// 假设我们有一个ElementRef实例
const myElement: ElementRef = ...;
// 使用Renderer2来创建一个新的div元素
const newDiv: any = this.renderer.createElement(myElement, 'div');
// 使用Renderer2来创建一个新的文本节点
const newText: any = this.renderer.createText(myElement, 'Hello, World!');
// 使用Renderer2来将一个新的类名添加到元素上
this.renderer.addClass(myElement, 'new-class');
// 使用Renderer2来从元素上移除一个类名
this.renderer.removeClass(myElement, 'old-class');
// 使用Renderer2来获取元素的属性值
const value = this.renderer.getAttribute(myElement, 'value');
// 使用Renderer2来设置元素的属性值
this.renderer.setAttribute(myElement, 'value', 'new value');
// 使用Renderer2来获取元素的样式值
const styleValue = this.renderer.getStyle(myElement, 'color');
// 使用Renderer2来设置元素的样式值
this.renderer.setStyle(myElement, 'color', 'red');
参考:
Angular开发实践(七): 跨平台操作DOM及渲染器Renderer2 | 技术之旅
API-元素: ng-container、ng-content 和 ng-template
ng-container、ng-template 模板变量
ng-container
是一个容器,它可以直接包裹任何元素,包括文本,但本身不会生成元素标签,也不会影响页面样式和布局。它的主要作用是与ngFor和ngIf等结构型指令配合使用,用于控制元素的渲染方式和布局。
一种特殊元素,可以在不向 DOM 添加新元素的情况下保存结构指令。
理解:如果需要不用 div,就是用文本,就使用 ng-container。
例子:这里 condition 为 true,就显示文本1,而没有在其他的结构下面,当然一般可以用 span。
<ng-container *ngIf="condition; else templateA">
1
</ng-container>
<ng-template #templateA>
2
</ng-template>
重复一个 HTML 块是一个很常见的用例,但前提是在特定条件为真时。一种直观的方法是将
*ngFor
和*ngIf
放在同一个元素上。但是,由于*NgFor
和*ngIf
都是结构指令,因此编译器会将其视为错误。你只能将一个 结构 指令应用于一个元素。原因是简单。结构指令可以用宿主元素及其后代做复杂的事情。
这个用例有一个简单的解决方案:将
*ngIf
放在包装*ngFor
元素的容器元素上。一个或两个元素可以是<ng-container>
,以便不会生成额外的 DOM 元素。****
ng-template
ng-template是Angular结构型指令中的一种,用于定义模板渲染HTML。定义的模板不会直接显示出来,需要通过其他结构型指令(如ngIf)或templateref将模块内容渲染到页面中。简单来说,ng-template就是用来定义模板的。
<ng-template #myTemplate>
<p>{{message}}</p>
</ng-template>
在上面的代码中,ng-template定义了一个名为myTemplate的模板,该模板包含一个p元素和其内容。通过其他结构型指令或templateref引用该模板,可以将该模板的内容渲染到页面中。
在组件类中访问 TemplateRef
在组件类中,你可以通过 @ViewChild
或 @ContentChild
装饰器来访问这个 TemplateRef
。
import { Component, ViewChild, TemplateRef, ViewContainerRef, Compiler, NgModuleRef, Injector } from '@angular/core';
@Component({
// ...
})
export class MyComponent {
@ViewChild('myTemplate') myTemplate: TemplateRef<any>;
constructor(
private viewContainerRef: ViewContainerRef,
private compiler: Compiler,
private moduleRef: NgModuleRef<any>,
private injector: Injector
) {}
// ...
}
动态地插入或渲染模板
在组件类中,你可以使用 ViewContainerRef
的 createEmbeddedView
方法来动态地插入或渲染 TemplateRef
。
ngAfterViewInit() {
this.viewContainerRef.createEmbeddedView(this.myTemplate);
}
除了直接在组件类中操作 TemplateRef
外,你还可以使用 Angular 提供的指令(如 *ngIf
、*ngFor
等)来间接地操作 TemplateRef
。这些指令内部都使用了 TemplateRef
和 ViewContainerRef
来实现动态渲染。
无条件投影 ng-content
ng-content 投影占位符
https://angular.cn/guide/content-projection#single-slot-content-projection
ng-content是内容映射指令,相当于vue中的slot内容分发。它用于在组件中嵌入模板代码,方便定制可复用的组件,并可以很好地扩充组件的功能,实现代码的复用。
内容投影 是从组件外部导入 HTML 内容,并把它插入在组件模板中指定位置上的一种途径。可以在目标中通过查找下列结构来认出内容投影。
-
元素标签中间的 HTML
-
组件模板中的
<ng-content>
标签
<ng-content>
标签是外来内容的占位符。它告诉 Angular 在哪里插入这些外来内容。在这里,被投影进去的内容就是来自父组件的<app-child>
标签。
Name Description select="selector"
Only select elements from the projected content that match the given CSS selector
.
仅从投影内容中选择与给定CSS “selector” 匹配的元素。Angular supports selectors for any combination of tag name, attribute, CSS class, and the :not
pseudo-class.
Angular支持对标签名、属性、CSS类和“:not”伪类的任意组合使用[selectors]。<ng-content select="样式类/HTML标签/指令"></ng-content> //仅从投影内容中选择与给定 CSS 匹配的元素selector
ng-content
是 Angular 中的一个重要特性,它允许你创建可重用的组件。这个特性可以让你把组件的一部分内容作为输入,然后在组件模板中的 <ng-content>
标签的位置插入这部分内容。这种方式可以让你在父组件中定义一些内容,然后在子组件的模板中显示这些内容。
ng-content
的使用非常简单,只需要在组件模板中添加 <ng-content></ng-content>
标签即可。然后在使用这个组件的地方,你可以在组件标签内部添加一些内容,这些内容会被插入到 <ng-content></ng-content>
的位置。
例子1:
假设你有一个名为 my-component
的组件,你希望在使用这个组件的地方可以自定义一些内容。你可以在 my-component
的模板中使用 <ng-content></ng-content>
:
<!--my-component.html-->
<div class="my-component">
<h2>这是我的组件</h2>
<ng-content></ng-content>
</div>
然后在使用 my-component
的地方,你可以在 my-component
标签内部添加一些内容:
<my-component>
<p>这是我自定义的内容</p>
</my-component>
在这个例子中,<p>这是我自定义的内容</p>
会被插入到 my-component
模板中 <ng-content></ng-content>
的位置。
例子2:
你也可以使用多个 <ng-content>
标签,并通过 select
属性来选择性地插入内容。例如:
<div class="my-component">
<h2>这是我的组件</h2>
<ng-content select=".title"></ng-content>
<ng-content select=".content"></ng-content>
</div>
然后在使用 my-component
的地方,你可以在 my-component
标签内部添加一些内容,并使用 class
来指定内容应该插入到哪个 <ng-content>
:
<my-component>
<p class="title">这是标题</p>
<p class="content">这是内容</p>
</my-component>
在这个例子中,<p class="title">这是标题</p>
会被插入到第一个 <ng-content>
的位置,<p class="content">这是内容</p>
会被插入到第二个 <ng-content>
的位置。
@ContentChildren
在Angular中,@ContentChildren
装饰器用于获取组件的内容投影(即<ng-content>
标签中包含的元素)。这些元素可以通过QueryList
对象来查询和管理。
要使用@ContentChildren获取内容投影中的元素,你需要将想要查询的DOM元素或指令的类型作为装饰器的参数。例如,如果你想要查询一个自定义指令MyDirective,你可以这样写:
import { Component, ContentChildren, QueryList } from '@angular/core';
import { MyDirective } from './my.directive';
@Component({
selector: 'my-component',
template: `
<ng-content></ng-content>
`
})
export class MyComponent {
@ContentChildren(MyDirective) myDirectives: QueryList<MyDirective>;
}
上面的代码中,@ContentChildren(MyDirective)装饰器告诉Angular要查找<ng-content>
标签中包含的所有MyDirective类型的指令,并将它们存储在myDirectives变量中。然后,你就可以使用QueryList提供的方法来查询和处理这些指令了。例如,你可以使用first()方法获取第一个指令,使用last()方法获取最后一个指令,使用length()方法获取指令列表的长度,使用map()、filter()、find()、reduce()等方法对指令列表进行操作,等等。
总之,@ContentChildren和QueryList是一对强大的组合,可以让你更加轻松地查询和管理组件内容投影中的DOM元素或指令。
如果是多插槽内容投影,则可以使用 @ContentChildren 获取投影元素的查询列表(QueryList)。
有条件投影 ngTemplateOutlet + ng-template
https://angular.cn/guide/content-projection#conditional-content-projection
如果你的组件需要有条件地渲染内容或多次渲染内容,则应配置该组件以接受一个 ng-template
元素,其中包含要有条件渲染的内容。
在这种情况下,不建议使用 ng-content
元素,因为只要组件的使用者提供了内容,即使该组件从未定义 ng-content
元素或该 <ng-content
元素位于 ngIf
语句的内部,该内容也总会被初始化。
使用 <ng-template
元素,你可以让组件根据你想要的任何条件显式渲染内容,并可以进行多次渲染。在显式渲染 <ng-template
元素之前,Angular 不会初始化该元素的内容。
ng-template
进行条件内容投影的典型实现。
<div *ngIf="expanded" [id]="contentId">
<ng-container [ngTemplateOutlet]="content.templateRef"></ng-container>
</div>
<ng-template appExampleZippyContent>
It depends on what you do with it.
</ng-template>
@ContentChild(ZippyContentDirective) content!: ZippyContentDirective;
@Directive({
selector: '[appExampleZippyContent]'
})
export class ZippyContentDirective {
constructor(public templateRef: TemplateRef<unknown>) {}
}
简单介绍 ngTemplateOutlet 的语法
HTML文件:
<ng-container *ngTemplateOutlet="tplStu; context: student"></ng-container>
<ng-template #tplStu let-name let-ageHTML="age">hello {{name}},your age is {{ageHTML}}</ng-template>
.ts文件
student = { $implicit: 'jack' , age: '19'};
按照官方文档,其语法知识如下:
*ngTemplateOutlet = "templateRefExp; context: contentExp"
templateRefExp
: ng-template 元素的#ID
contextExp
:1、可空参数;
2、可以在模版中使用
let-key
语句进行绑定; 这个key,是我们在HTML模版绑定显示的key,即 {{key}} 。3、使用
$implicit
这个key会把对应的值设置为默认值;因为我们会为
context
指定一个对象,每个对象可能有一个或多个值;如果这个对象,是一个值,则不需显示指定名称,如 student : { name: 'jack' } ,可以用 student: { $implicit: 'jack' };
在 ng-template 中也不必使用 let-showName = "name", 只需要 let-showName 即可。
ngTemplateOutlet 的使用场景和作用
ngTemplateOutlet
是 Angular 框架中的一个指令,它允许你在组件的模板中动态地渲染和插入其他模板的内容。ngTemplateOutlet
的主要作用是将一个预先定义的TemplateRef
插入到当前组件的视图中。以下是
ngTemplateOutlet
的一些使用场景和作用:
- 动态内容渲染:你可以根据某些条件或逻辑动态地插入不同的模板内容。例如,你可能有一个列表组件,它可以根据列表项的类型动态地渲染不同的模板。
- 组件间内容共享:
ngTemplateOutlet
可以与ngContent
结合使用,实现父组件和子组件之间的内容共享。父组件可以定义一些模板,并通过ngContent
将这些模板传递给子组件。子组件则可以使用ngTemplateOutlet
来渲染这些传递过来的模板。- 动态表单字段:在构建动态表单时,你可能需要根据表单项的类型(如文本、下拉框、复选框等)动态地渲染不同的表单字段模板。使用
ngTemplateOutlet
可以轻松实现这一功能。- 自定义指令或组件:如果你正在开发一个自定义指令或组件,并希望它能够接受外部提供的模板作为其内容的一部分,那么
ngTemplateOutlet
将是一个很好的选择。在使用
ngTemplateOutlet
时,你需要提供一个TemplateRef
类型的参数,该参数指定了要插入的模板的引用。你可以通过视图查询(ViewChild)或内容查询(ContentChild)来获取这个引用。此外,
ngTemplateOutlet
还支持一个可选的context
参数,该参数允许你向插入的模板传递一些上下文数据。这些数据可以在模板中通过let
语法进行访问和使用。<!-- 父组件模板 --> <app-child> <ng-template #myTemplate let-item="item"> <p>{{ item.name }}</p> </ng-template> </app-child> <!-- 子组件模板 --> <div> <ng-container *ngTemplateOutlet="passedTemplate; context: { item: myItem }"></ng-container> </div> // 子组件类 @Component({ ... }) export class ChildComponent { @ContentChild(TemplateRef) passedTemplate: TemplateRef<any>; myItem = { name: 'Hello, World!' }; }
在上面的示例中,父组件定义了一个名为
myTemplate
的模板,并通过内容投影将其传递给子组件。子组件通过@ContentChild
装饰器获取到这个模板的引用,并使用ngTemplateOutlet
指令将其插入到自己的视图中。同时,子组件还向模板传递了一个名为item
的上下文数据。在模板中,我们可以通过let-item="item"
语法来访问这个数据。
HostListener
@HostBinding 用于在宿主元素设置属性绑定,并应用于指令属性。
@HostListener 用于在宿主元素设置事件绑定,并应用于指令方法。
ViewContainerRef
TempElemetRef
EventEmitter
@Output()
@Input()
NgNonBindable 停用绑定