如何在组件模板中选择元素?

734

有人知道如何获取组件模板中定义的元素吗?在Polymer中,使用 $$$ 很容易实现。

我只是想知道在Angular中该如何操作。

以教程中的示例为例:

import {Component} from '@angular/core';

@Component({
    selector:'display',
    template:`
     <input #myname (input)="updateName(myname.value)"/>
     <p>My name : {{myName}}</p>
     `   
})
export class DisplayComponent {
    myName: string = "Aman";
    updateName(input: String) {
        this.myName = input;
    }
}

我该如何在类定义内部获取或捕获 pinput 元素的引用?

15个回答

1235

不必注入ElementRef并从那里使用querySelector或类似的方法,而是可以直接使用声明式方式来访问视图中的元素:

<input #myname>

@ViewChild('myname') input; 

元素
ngAfterViewInit() {
  console.log(this.input.nativeElement.value);
}

StackBlitz示例

  • @ViewChild()支持指令或组件类型作为参数,或模板变量的名称(字符串)。
  • @ViewChildren()还支持以逗号分隔的名称列表(目前不允许使用空格 @ViewChildren('var1,var2,var3'))。
  • @ContentChild()@ContentChildren()在轻DOM中执行相同的操作(投影元素<ng-content>)。

后代

@ContentChildren()是唯一一个允许您查询后代的方法。

@ContentChildren(SomeTypeOrVarName, {descendants: true}) someField; 

`{descendants: true}`应该是默认值,但在2.0.0最终版本中不是,并且被认为是一个错误。
这个问题已在2.0.1中修复。
read
如果存在一个组件和指令,`read`参数允许您指定应返回哪个实例。
例如,对于动态创建的组件,可以使用`ViewContainerRef`而不是默认的`ElementRef`。
@ViewChild('myname', { read: ViewContainerRef }) target;

订阅更改

尽管视图子元素只在调用ngAfterViewInit()时设置,内容子元素只在调用ngAfterContentInit()时设置,但如果要订阅查询结果的更改,应该在ngOnInit()中进行。

https://github.com/angular/angular/issues/9689#issuecomment-229247134

@ViewChildren(SomeType) viewChildren;
@ContentChildren(SomeType) contentChildren;

ngOnInit() {
  this.viewChildren.changes.subscribe(changes => console.log(changes));
  this.contentChildren.changes.subscribe(changes => console.log(changes));
}

直接访问 DOM

只能查询 DOM 元素,而不能查询组件或指令实例:

export class MyComponent {
  constructor(private elRef:ElementRef) {}
  ngAfterViewInit() {
    var div = this.elRef.nativeElement.querySelector('div');
    console.log(div);
  }

  // for transcluded content
  ngAfterContentInit() {
    var div = this.elRef.nativeElement.querySelector('div');
    console.log(div);
  }
}

获取任意投影内容
查看访问被引用的内容

15
角度团队建议避免使用ElementRef,这是更好的解决方案。 - Honorable Chow
9
实际上,input 也是一个 ElementRef,但你可以获取到你实际想要的元素的引用,而不是从宿主 ElementRef 查询它。 - Günter Zöchbauer
49
实际上,使用 ElementRef 是没问题的。同时,使用带有 RendererElementRef.nativeElement 也是可以的。被不鼓励的是直接访问 ElementRef.nativeElement.xxx 属性。 - Günter Zöchbauer
2
@Natanael 我不知道这个是否有明确的文档说明,但在问题或其他讨论中经常提到(也来自Angular团队成员),应避免直接访问DOM。直接访问DOM(即访问ElementRef.nativeElement的属性和方法)会阻止您使用Angular的服务器端渲染和WebWorker功能(我不知道它是否也会破坏即将推出的离线模板编译器 - 但我猜不会)。 - Günter Zöchbauer
11
如上所述,在“读取”部分中,如果您想通过ViewChild获取一个元素的nativeElement,则需要执行以下操作: @ViewChild('myObj', { read: ElementRef }) myObj: ElementRef; - jsgoupil
显示剩余17条评论

228

1
@Brocco,你能更新一下你的答案吗?因为ElementRef已经被移除了,所以我想看到一个当前的解决方案。 - Jefftopia
26
ElementRef 可用了(再次?)。 - Günter Zöchbauer
10
只有在必须直接访问DOM时,才使用此API作为最后的手段。请使用Angular提供的模板和数据绑定。或者您可以查看Renderer,它提供了API,即使不支持对原生元素的直接访问也可以安全使用。 依赖直接访问DOM会创建应用程序和渲染层之间的紧密耦合,这将使两者无法分离并将应用程序部署到网络工作者中。 - sandeep talabathula
@sandeeptalabathula,对于从第三方库中附加浮动日期选择器组件,有什么更好的选项来查找元素?我知道这不是最初的问题,但你认为在所有情况下在DOM中查找元素都是不好的。 - ProgrammingLlama
15
@john 哦,好的。你可以尝试这个 - this.element.nativeElement.querySelector('#someElementId') 并像这样将 ElementRef 传递给构造函数... private element: ElementRef, 导入库... import { ElementRef } from '@angular/core'; - sandeep talabathula
显示剩余3条评论

60
import { Component, ElementRef, OnInit } from '@angular/core';

@Component({
  selector:'display',
  template:`
   <input (input)="updateName($event.target.value)">
   <p> My name : {{ myName }}</p>
  `
})
class DisplayComponent implements OnInit {
  constructor(public element: ElementRef) {
    this.element.nativeElement // <- your direct element reference 
  }
  ngOnInit() {
    var el = this.element.nativeElement;
    console.log(el);
  }
  updateName(value) {
    // ...
  }
}

示例已更新以适应最新版本

有关原生元素的更多详细信息,请点击这里


21

Angular 4+: 使用 CSS 选择器和 renderer.selectRootElement 方法来访问元素。

我有一个表单,最初只显示电子邮件输入框。在输入电子邮件后,该表单将扩展以允许他们继续添加与其项目相关的信息。但是,如果他们不是现有客户,则该表单将在项目信息部分上方包括地址部分。

截至目前,数据输入部分未分解为组件,因此使用 *ngIf 指令来管理这些部分。如果他们是现有客户,则需要将焦点设置在项目注释字段上,否则需要将焦点设置在名字字段上。

我尝试了一些解决方案,但没有成功。然而,在这个答案中的第三个更新给了我一半的解决方案。另一半来自于 MatteoNY 在这个主题中的回复。结果如下:

import { NgZone, Renderer } from '@angular/core';

constructor(private ngZone: NgZone, private renderer: Renderer) {}

setFocus(selector: string): void {
    this.ngZone.runOutsideAngular(() => {
        setTimeout(() => {
            this.renderer.selectRootElement(selector).focus();
        }, 0);
    });
}

submitEmail(email: string): void {
    // Verify existence of customer
    ...
    if (this.newCustomer) {
        this.setFocus('#firstname');
    } else {
        this.setFocus('#description');
    }
}

由于我所做的唯一事情就是设置元素的焦点,所以我不需要关注变更检测,因此我实际上可以在 Angular 之外运行调用 renderer.selectRootElement。由于我需要给新部分渲染时间,因此元素部分被包裹在超时中,以允许渲染线程在尝试选择元素之前赶上时间。一旦所有这些都设置好了,我可以使用基本的 CSS 选择器简单地调用该元素。

我知道这个例子主要涉及焦点事件,但对我来说很难想象它不能用于其他环境。

更新: Angular 在 Angular 4 中放弃了对 Renderer 的支持,并在 Angular 9 中完全删除了它。这种解决方案不应受迁移到 Renderer2 的影响。请参阅此链接获取更多信息: Renderer 迁移到 Renderer2


自Angular 4.3.0起,类Renderer已被弃用。https://angular.io/api/core/Renderer - Jamie
1
我们能不能只使用Renderer2 @Jamie?https://angular.io/api/core/Renderer2#selectRootElement - Jordan

19

如果要在*ngIf*ngSwitchCase中获取组件实例,您可以尝试使用以下技巧:

创建一个init指令。

import {
    Directive,
    EventEmitter,
    Output,
    OnInit,
    ElementRef
} from '@angular/core';

@Directive({
    selector: '[init]'
})
export class InitDirective implements OnInit {
    constructor(private ref: ElementRef) {}

    @Output() init: EventEmitter<ElementRef> = new EventEmitter<ElementRef>();

    ngOnInit() {
        this.init.emit(this.ref);
    }
}

将您的组件导出并赋予名称,例如 myComponent

@Component({
    selector: 'wm-my-component',
    templateUrl: 'my-component.component.html',
    styleUrls: ['my-component.component.css'],
    exportAs: 'myComponent'
})
export class MyComponent { ... }

使用此模板可获取ElementRefMyComponent实例。

<div [ngSwitch]="type">
    <wm-my-component
           #myComponent="myComponent"
           *ngSwitchCase="Type.MyType"
           (init)="init($event, myComponent)">
    </wm-my-component>
</div>

在 TypeScript 中使用此代码

init(myComponentRef: ElementRef, myComponent: MyComponent) {
}

16

@angular/core导入ViewChild装饰器,像这样:

HTML代码:

<form #f="ngForm"> 
  ... 
  ... 
</form>

TS 代码:

import { ViewChild, ElementRef} from '@angular/core';

class TemplateFormComponent {

  @ViewChild('f') myForm: ElementRef;
    .
    .
    .
}

现在你可以使用“myForm”对象在类中访问其中的任何元素。

源代码


但是你应该注意到,在组件类中几乎不需要访问模板元素,你只需要正确理解Angular逻辑即可。 - Hany
8
不使用任何,类型为ElementRef。 - user89862
最终我不得不使用“any”,因为我需要访问的元素是另一个Angular组件,它包装了一个Kendo UI元素,我需要调用该组件上的一个方法,然后调用Kendo元素上的一个方法。 - TomEberhard

13
 */
import {Component,ViewChild} from '@angular/core' /*Import View Child*/

@Component({
    selector:'display'
    template:`

     <input #myname (input) = "updateName(myname.value)"/>
     <p> My name : {{myName}}</p>

    `
})
export class DisplayComponent{
  @ViewChild('myname')inputTxt:ElementRef; /*create a view child*/

   myName: string;

    updateName: Function;
    constructor(){

        this.myName = "Aman";
        this.updateName = function(input: String){

            this.inputTxt.nativeElement.value=this.myName; 

            /*assign to it the value*/
        };
    }
}

12
请解释一下这段代码。不要简单地把代码倒出来,需要提供解释。简单地倾倒代码而没有解释是不被鼓励的。 - rayryeng
5
这样做不行:通过@ViewChild注解设置的属性只有在ngAfterViewInit生命周期事件之后才可用。如果在构造函数中访问该值,则会导致inputTxt的值为未定义。 - David M.

9

我使用了两种方法:

第一种方式:

您可以通过将ElementRef注入到组件的构造函数中来获取对DOM元素的句柄:

constructor(private myElement: ElementRef) {
this.myElement.nativeElement // <- your direct element reference
}

第二种方法:

@Component({
  selector: 'my-app',
  template:
  `
  <input #input value="enterThere">
  `,
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  @ViewChild('input') input:ElementRef; 

  ngAfterViewInit() {
    console.log(this.input);
  }

有没有办法引用隐藏的元素?因为我正在尝试,但不起作用。我的意思是模板中具有 [hidden]="<condition>" 的元素。 - acarlstein

5
注意:此内容不适用于Angular 6及以上版本,因为ElementRef变成了带有T表示nativeElement类型的ElementRef<T>
我想补充一点,如果你正在使用建议中的ElementRef,那么你会立即遇到一个问题,即ElementRef具有糟糕的类型声明,看起来像是这样的。
export declare class ElementRef {
  nativeElement: any;
}

在浏览器环境中,当nativeElement是HTMLElement时,这很愚蠢。

要解决此问题,您可以使用以下技术:

import {Inject, ElementRef as ErrorProneElementRef} from '@angular/core';

interface ElementRef {
  nativeElement: HTMLElement;
}

@Component({...}) export class MyComponent {
  constructor(@Inject(ErrorProneElementRef) readonly elementRef: ElementRef) { }
}

1
这解释了我遇到的一个问题。这不起作用,因为它会说item需要是一个ElementRef,即使你将其设置为另一个ElementRef:let item: ElementRef, item2: ElementRef; item = item2; // 不行。非常令人困惑。但这样就可以了:let item: ElementRef, item2: ElementRef; item = item2.nativeElement,因为你指出了实现方式。 - oooyaya
1
实际上,你的第一个例子 let item: ElementRef, item2: ElementRef; item = item2 会因为明确赋值分析而失败。你的第二个例子也会因为同样的原因而失败,但如果像我们讨论的那样初始化 item2,或者作为可分配性的有用快速检查,我们可以在这里使用 declare let 来使两个例子都成功。无论如何,在公共 API 中看到 any 真是太遗憾了。 - Aluan Haddad

3

快速使用的最小示例:

import { Component, ElementRef, ViewChild} from '@angular/core';

@Component({
  selector: 'my-app',
  template:
  `
  <input #inputEl value="hithere">
  `,
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  @ViewChild('inputEl') inputEl:ElementRef; 

  ngAfterViewInit() {
    console.log(this.inputEl);
  }
}
  1. 在感兴趣的DOM元素上放置一个模板引用变量。在我们的示例中,这是<input>标签上的#inputEl
  2. 在我们的组件类中通过@ViewChild装饰器注入DOM元素。
  3. ngAfterViewInit生命周期钩子中访问该元素。

注意:

如果您想操作DOM元素,请使用Renderer2 API而不是直接访问元素。允许直接访问DOM可能会使您的应用程序更容易受到XSS攻击。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接