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具有以下特点:

  1. 当应用程序的状态发生变化时,Angular会自动为QueryList更新对象项。
  2. QueryList中的每个对象都是一个项目列表对象,可以使用first、last、length、map、filter、find、reduce等方法进行操作。
  3. 可以通过toArray()方法将QueryList转换为数组形式。
  4. 可以使用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 的一些使用场景和作用:

  1. 动态内容渲染:你可以根据某些条件或逻辑动态地插入不同的模板内容。例如,你可能有一个列表组件,它可以根据列表项的类型动态地渲染不同的模板。
  2. 组件间内容共享ngTemplateOutlet 可以与 ngContent 结合使用,实现父组件和子组件之间的内容共享。父组件可以定义一些模板,并通过 ngContent 将这些模板传递给子组件。子组件则可以使用 ngTemplateOutlet 来渲染这些传递过来的模板。
  3. 动态表单字段:在构建动态表单时,你可能需要根据表单项的类型(如文本、下拉框、复选框等)动态地渲染不同的表单字段模板。使用 ngTemplateOutlet 可以轻松实现这一功能。
  4. 自定义指令或组件:如果你正在开发一个自定义指令或组件,并希望它能够接受外部提供的模板作为其内容的一部分,那么 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 停用绑定

posted @ 2023-09-21 23:47  【唐】三三  阅读(122)  评论(0编辑  收藏  举报