Angular 2 @ViewChild批注返回未定义


241

我正在尝试学习Angular 2。

我想使用@ViewChild Annotation 从父组件访问子组件。

下面是几行代码:

BodyContent.ts中,我有:

import {ViewChild, Component, Injectable} from 'angular2/core';
import {FilterTiles} from '../Components/FilterTiles/FilterTiles';


@Component({
selector: 'ico-body-content'
, templateUrl: 'App/Pages/Filters/BodyContent/BodyContent.html'
, directives: [FilterTiles] 
})


export class BodyContent {
    @ViewChild(FilterTiles) ft:FilterTiles;

    public onClickSidebar(clickedElement: string) {
        console.log(this.ft);
        var startingFilter = {
            title: 'cognomi',
            values: [
                'griffin'
                , 'simpson'
            ]}
        this.ft.tiles.push(startingFilter);
    } 
}

FilterTiles.ts中

 import {Component} from 'angular2/core';


 @Component({
     selector: 'ico-filter-tiles'
    ,templateUrl: 'App/Pages/Filters/Components/FilterTiles/FilterTiles.html'
 })


 export class FilterTiles {
     public tiles = [];

     public constructor(){};
 }

最后是模板(如注释中所建议):

BodyContent.html

<div (click)="onClickSidebar()" class="row" style="height:200px; background-color:red;">
        <ico-filter-tiles></ico-filter-tiles>
    </div>

FilterTiles.html

<h1>Tiles loaded</h1>
<div *ngFor="#tile of tiles" class="col-md-4">
     ... stuff ...
</div>

FilterTiles.html模板已正确加载到 ico-filter-tiles标记中(实际上,我能够看到标头)。

注意:BodyContent类使用DynamicComponetLoader注入到另一个模板(Body)内:dcl.loadAsRoot(BodyContent,'#ico-bodyContent',注入器):

import {ViewChild, Component, DynamicComponentLoader, Injector} from 'angular2/core';
import {Body}                 from '../../Layout/Dashboard/Body/Body';
import {BodyContent}          from './BodyContent/BodyContent';

@Component({
    selector: 'filters'
    , templateUrl: 'App/Pages/Filters/Filters.html'
    , directives: [Body, Sidebar, Navbar]
})


export class Filters {

    constructor(dcl: DynamicComponentLoader, injector: Injector) {
       dcl.loadAsRoot(BodyContent, '#ico-bodyContent', injector);
       dcl.loadAsRoot(SidebarContent, '#ico-sidebarContent', injector);

   } 
}

问题是,当我尝试写入ft控制台日志时,我得到了undefined,当然,当我尝试将某些内容推入“ tiles”数组中时,我当然会遇到异常:“ no属性tile为“ undefined””

还有一件事:FilterTiles组件似乎已正确加载,因为我能够看到它的html模板。

有什么建议吗?谢谢


看起来正确。模板中可能包含一些内容,但您的问题中未包含它。
君特Zöchbauer

1
与Günter达成协议。我用您的代码和简单的关联模板创建了一个plunkr,它可以正常工作。看到此链接:plnkr.co/edit/KpHp5Dlmppzo1LXcutPV?p=preview。我们需要模板;-)
Thierry Templier,2016年

1
ft不会在构造函数中设置,但在click事件处理程序中将已经设置。
君特Zöchbauer

5
您正在使用loadAsRoot,它具有更改检测的已知问题。只是要确保尝试使用loadNextToLocationloadIntoLocation
埃里克·马丁内斯

1
问题是loadAsRoot。一旦我替换loadIntoLocation为问题就解决了。如果您将您的评论作为答案,我可以将其标记为接受
Andrea Ialenti

Answers:


371

我遇到了类似的问题,并认为如果有人犯同样的错误,我会发帖。首先,要考虑的一件事是AfterViewInit;您需要等待视图初始化后才能访问@ViewChild。但是,我@ViewChild仍然返回null。问题是我的*ngIf。该*ngIf指令杀死了我的控件组件,因此我无法引用它。

import {Component, ViewChild, OnInit, AfterViewInit} from 'angular2/core';
import {ControlsComponent} from './controls/controls.component';
import {SlideshowComponent} from './slideshow/slideshow.component';

@Component({
    selector: 'app',
    template:  `
        <controls *ngIf="controlsOn"></controls>
        <slideshow (mousemove)="onMouseMove()"></slideshow>
    `,
    directives: [SlideshowComponent, ControlsComponent]
})

export class AppComponent {
    @ViewChild(ControlsComponent) controls:ControlsComponent;

    controlsOn:boolean = false;

    ngOnInit() {
        console.log('on init', this.controls);
        // this returns undefined
    }

    ngAfterViewInit() {
        console.log('on after view init', this.controls);
        // this returns null
    }

    onMouseMove(event) {
         this.controls.show();
         // throws an error because controls is null
    }
}

希望能有所帮助。

编辑
下面@Ashg所述,一种解决方案是使用@ViewChildren而不是@ViewChild


9
@kenecaswell所以您找到了解决问题的更好方法。我也面临着同样的问题。我有很多* ngIf,因此该元素毕竟只有在true内,但是我需要该元素引用。解决这个问题的任何方法>
monica

4
我发现如果使用ngIf,子组件在ngAfterViewInit()中是“未定义”的。我尝试设置较长的超时时间,但仍然没有效果。但是,子组件稍后可用(即响应单击事件等)。如果我不使用ngIf,并且它在ngAfterViewInit()中按预期定义。还有更多的父/子通信这里angular.io/docs/ts/latest/cookbook/...
马修·赫加蒂

3
我用bootstrap ngClass+ hiddenclass代替ngIf。那行得通。谢谢!
Rahmathullah M

9
这不能解决问题,请使用下面的解决方案,使用@ViewChildren在子控件可用时获取对子控件的引用
Ashg

20
这只是证明了“问题”,对吧?它没有发布解决方案。
米格尔·里贝罗

144

前面提到的问题ngIf是导致视图未定义的。答案是使用ViewChildren而不是ViewChild。我有一个类似的问题,在所有参考数据都已加载之前,我不希望显示网格。

的HTML:

   <section class="well" *ngIf="LookupData != null">
       <h4 class="ra-well-title">Results</h4>
       <kendo-grid #searchGrid> </kendo-grid>
   </section>

组件代码

import { Component, ViewChildren, OnInit, AfterViewInit, QueryList  } from '@angular/core';
import { GridComponent } from '@progress/kendo-angular-grid';

export class SearchComponent implements OnInit, AfterViewInit
{
    //other code emitted for clarity

    @ViewChildren("searchGrid")
    public Grids: QueryList<GridComponent>

    private SearchGrid: GridComponent

    public ngAfterViewInit(): void
    {

        this.Grids.changes.subscribe((comps: QueryList <GridComponent>) =>
        {
            this.SearchGrid = comps.first;
        });


    }
}

在这里,我们ViewChildren可以用来监听更改。在这种情况下,任何孩子都有参考#searchGrid。希望这可以帮助。


4
在某些情况下,我想补充一点,例如。this.SearchGrid属性,您应使用setTimeout(()=>{ ///your code here }, 1); 避免出现异常的语法异常:检查表达式后,表达式已更改
rafalkasa

3
如果要将#searchGrid标记放置在普通HTML元素而不是Angular2元素上,该怎么办?(例如,<div #searchGrid> </ div>,它在* ngIf块内部吗?
Vern Jensen

1
这是我用例的正确答案!谢谢,我需要访问一个组件,因为它可以通过ngIf =获得
Frozen_byte

1
这对于ajax响应(现在可以正常工作)非常完美*ngIf,在渲染之后,我们可以从动态组件中保存ElementRef。
elporfirio

4
同样不要忘记将其分配给订阅,然后取消订阅
tam.teixeira

63

您可以使用二传手 @ViewChild()

@ViewChild(FilterTiles) set ft(tiles: FilterTiles) {
    console.log(tiles);
};

如果您具有ngIf包装器,则将使用未定义的setter进行调用,然后使用ngIf允许它的渲染再次引用。

我的问题是别的。我没有在我的app.modules中包含包含“ FilterTiles”的模块。模板没有引发错误,但是引用始终是未定义的。


3
这对我不起作用-我得到第一个未定义的消息,但没有得到带有引用的第二个调用。应用程式是ng2 ...这是ng4 +功能吗?
周杰伦·康明斯'18

@Jay我相信这是因为在这种情况下,您尚未向Angular注册该组件FilterTiles。出于这个原因,我之前遇到过该问题。
国会

1
作品角8使用HTML元素和注释像#paginator@ViewChild('paginator', {static: false})
Qiteq

1
这是ViewChild更改的回调吗?
Yasser Nascimento

24

这对我有用。

例如,使用* ngIf =“ showMe”来显示名为“ my-component”的组件,如下所示:

<my-component [showMe]="showMe" *ngIf="showMe"></my-component>

因此,在初始化组件时,直到“ showMe”为真时才显示该组件。因此,我的@ViewChild引用都是未定义的。

这是我使用@ViewChildren及其返回的QueryList的地方。请参阅有关QueryList的角度文章和@ViewChildren用法演示

您可以使用@ViewChildren返回的QueryList并使用rxjs订阅对引用项的任何更改,如下所示。@ViewChild没有此功能。

import { Component, ViewChildren, ElementRef, OnChanges, QueryList, Input } from '@angular/core';
import 'rxjs/Rx';

@Component({
    selector: 'my-component',
    templateUrl: './my-component.component.html',
    styleUrls: ['./my-component.component.css']
})
export class MyComponent implements OnChanges {

  @ViewChildren('ref') ref: QueryList<any>; // this reference is just pointing to a template reference variable in the component html file (i.e. <div #ref></div> )
  @Input() showMe; // this is passed into my component from the parent as a    

  ngOnChanges () { // ngOnChanges is a component LifeCycle Hook that should run the following code when there is a change to the components view (like when the child elements appear in the DOM for example)
    if(showMe) // this if statement checks to see if the component has appeared becuase ngOnChanges may fire for other reasons
      this.ref.changes.subscribe( // subscribe to any changes to the ref which should change from undefined to an actual value once showMe is switched to true (which triggers *ngIf to show the component)
        (result) => {
          // console.log(result.first['_results'][0].nativeElement);                                         
          console.log(result.first.nativeElement);                                          

          // Do Stuff with referenced element here...   
        } 
      ); // end subscribe
    } // end if
  } // end onChanges 
} // end Class

希望这可以帮助某人节省一些时间和挫败感。


3
实际上,您的解决方案似乎是迄今为止列出的最佳方法。注意我们必须记住,现在已经不推荐使用排名前73的解决方案...因为Angular 4中不再支持伪指令:[...]声明,所以它在Angular 4
中将不起作用

5
不要忘记退订或使用.take(1).subscribe(),但出色的答案,非常感谢!
布莱尔·康诺利

2
优秀的解决方案。我已在ngAfterViewInit()中订阅了ref更改,而不是ngOnChanges()。但我必须添加setTimeout才能摆脱ExpressionChangedAfterChecked错误
Josf

这应该标记为实际解决方案。非常感谢!
platzhersh

10

我的解决方法是使用[style.display]="getControlsOnStyleDisplay()"而不是*ngIf="controlsOn"。块在那里,但不显示。

@Component({
selector: 'app',
template:  `
    <controls [style.display]="getControlsOnStyleDisplay()"></controls>
...

export class AppComponent {
  @ViewChild(ControlsComponent) controls:ControlsComponent;

  controlsOn:boolean = false;

  getControlsOnStyleDisplay() {
    if(this.controlsOn) {
      return "block";
    } else {
      return "none";
    }
  }
....

有一个页面,其中根据showList变量的值在表中显示项目列表,或显示编辑项目。通过结合使用[style.display] =“!showList”和* ngIf =“!showList”,摆脱了令人讨厌的控制台错误。
razvanone

8

我对这个解决办法是更换*ngIf[hidden]。缺点是所有子组件都存在于代码DOM中。但是满足了我的要求。


8

解决我的问题的是确保static将设置为false

@ViewChild(ClrForm, {static: false}) clrForm;

随着static关闭,@ViewChild参考得到由角时更新的*ngIf指令改变。


这几乎是一个完美的答案,要指出也是检查可空值的一个好习惯,所以我们最终得到这样的结果:@ViewChild(ClrForm,{static:false})set clrForm(clrForm:ClrForm){如果(clrForm){this.clrForm = clrForm; };
克劳斯·克莱因

5

就我而言,我有一个使用的输入变量设置器ViewChild,而in ViewChild位于*ngIf指令内部,因此该设置器尝试在*ngIf呈现之前访问它(如果不使用*ngIf,它可以正常工作,但是如果始终将其设置为真有*ngIf="true")。

为了解决这个问题,我使用Rxjs来确保ViewChild在启动视图之前已对等待的所有引用。首先,创建一个在视图初始化后完成的主题。

export class MyComponent implements AfterViewInit {
  private _viewInitWaiter$ = new Subject();

  ngAfterViewInit(): void {
    this._viewInitWaiter$.complete();
  }
}

然后,创建一个函数,该函数在主题完成后接受并执行lambda。

private _executeAfterViewInit(func: () => any): any {
  this._viewInitWaiter$.subscribe(null, null, () => {
    return func();
  })
}

最后,确保对ViewChild的引用使用此功能。

@Input()
set myInput(val: any) {
    this._executeAfterViewInit(() => {
        const viewChildProperty = this.viewChild.someProperty;
        ...
    });
}

@ViewChild('viewChildRefName', {read: MyViewChildComponent}) viewChild: MyViewChildComponent;

1
这比所有的settimeout nonesense都要好得多
Liam

4

它必须工作。

但是正如GünterZöchbauer所说,模板中肯定还有其他问题。我创建了一个相关的Plunkr-Answer。请检查浏览器的控制台。

引导程序

@Component({
selector: 'my-app'
, template: `<div> <h1> BodyContent </h1></div>

      <filter></filter>

      <button (click)="onClickSidebar()">Click Me</button>
  `
, directives: [FilterTiles] 
})


export class BodyContent {
    @ViewChild(FilterTiles) ft:FilterTiles;

    public onClickSidebar() {
        console.log(this.ft);

        this.ft.tiles.push("entered");
    } 
}

filterTiles.ts

@Component({
     selector: 'filter',
    template: '<div> <h4>Filter tiles </h4></div>'
 })


 export class FilterTiles {
     public tiles = [];

     public constructor(){};
 }

它像一种魅力。请仔细检查您的标签和参考。

谢谢...


1
如果问题是我的一样,复制你需要把* ngIf围绕模板<过滤器> </过滤器> ..显然,如果ngIf返回false,ViewChild没有得到有线并返回null
丹追逐

2

这对我有用,请参见下面的示例。

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

@Component({
    selector: 'app',
    template:  `
        <a (click)="toggle($event)">Toggle</a>
        <div *ngIf="visible">
          <input #control name="value" [(ngModel)]="value" type="text" />
        </div>
    `,
})

export class AppComponent {

    private elementRef: ElementRef;
    @ViewChild('control') set controlElRef(elementRef: ElementRef) {
      this.elementRef = elementRef;
    }

    visible:boolean;

    toggle($event: Event) {
      this.visible = !this.visible;
      if(this.visible) {
        setTimeout(() => { this.elementRef.nativeElement.focus(); });
      }
    }

}


2

我有一个类似的问题,其中子句位于在引用之前未加载viewChild元素ViewChildswitch子句中。我以半hacky的方式解决了它,但是将ViewChild引用包装在setTimeout立即执行的引用中(即0ms)


1

我对此的解决方案是将ngIf从子组件的外部移到包裹了整个html的div上的子组件的内部。这样,它仍然在需要时被隐藏起来,但是能够加载该组件,我可以在父级中引用它。


但是为此,您如何获得父级中的“可见”变量?
丹·蔡斯

1

我修复了它,只是在设置可见组件后添加SetTimeout

我的HTML:

<input #txtBus *ngIf[show]>

我的组件JS

@Component({
  selector: "app-topbar",
  templateUrl: "./topbar.component.html",
  styleUrls: ["./topbar.component.scss"]
})
export class TopbarComponent implements OnInit {

  public show:boolean=false;

  @ViewChild("txtBus") private inputBusRef: ElementRef;

  constructor() {

  }

  ngOnInit() {}

  ngOnDestroy(): void {

  }


  showInput() {
    this.show = true;
    setTimeout(()=>{
      this.inputBusRef.nativeElement.focus();
    },500);
  }
}

1

就我而言,我知道子组件始终存在,但想在子初始化之前更改状态以节省工作。

我选择对子项进行测试,直到它出现并立即进行更改,这为我节省了子项组件的更改周期。

export class GroupResultsReportComponent implements OnInit {

    @ViewChild(ChildComponent) childComp: ChildComponent;

    ngOnInit(): void {
        this.WhenReady(() => this.childComp, () => { this.childComp.showBar = true; });
    }

    /**
     * Executes the work, once the test returns truthy
     * @param test a function that will return truthy once the work function is able to execute 
     * @param work a function that will execute after the test function returns truthy
     */
    private WhenReady(test: Function, work: Function) {
        if (test()) work();
        else setTimeout(this.WhenReady.bind(window, test, work));
    }
}

明智地,您可以添加最大尝试次数或向添加几毫秒的延迟setTimeoutsetTimeout有效地将该函数置于待处理操作列表的底部。


0

一种通用方法:

您可以创建一个方法,直到ViewChild准备就绪

function waitWhileViewChildIsReady(parent: any, viewChildName: string, refreshRateSec: number = 50, maxWaitTime: number = 3000): Observable<any> {
  return interval(refreshRateSec)
    .pipe(
      takeWhile(() => !isDefined(parent[viewChildName])),
      filter(x => x === undefined),
      takeUntil(timer(maxWaitTime)),
      endWith(parent[viewChildName]),
      flatMap(v => {
        if (!parent[viewChildName]) throw new Error(`ViewChild "${viewChildName}" is never ready`);
        return of(!parent[viewChildName]);
      })
    );
}


function isDefined<T>(value: T | undefined | null): value is T {
  return <T>value !== undefined && <T>value !== null;
}

用法:

  // Now you can do it in any place of your code
  waitWhileViewChildIsReady(this, 'yourViewChildName').subscribe(() =>{
      // your logic here
  })

0

对我来说,问题是我引用了元素上的ID。

@ViewChild('survey-form') slides:IonSlides;

<div id="survey-form"></div>

而不是像这样:

@ViewChild('surveyForm') slides:IonSlides;

<div #surveyForm></div>

0

如果您使用的是Ionic,则需要使用ionViewDidEnter()生命周期挂钩。离子运行一些额外的东西(主要是与动画相关的),这通常会导致意想不到的错误就是这样,因此需要的东西,运行 ngOnInitngAfterContentInit等。


-1

这对我有用。

@ViewChild('mapSearch', { read: ElementRef }) mapInput: ElementRef;

ngAfterViewInit() {
  interval(1000).pipe(
        switchMap(() => of(this.mapInput)),
        filter(response => response instanceof ElementRef),
        take(1))
        .subscribe((input: ElementRef) => {
          //do stuff
        });
}

所以我基本上每隔一秒钟进行一次检查,直到*ngIf变成真,然后我做与​​清单有关的事情ElementRef


-3

对我有用的解决方案是将指令添加到app.module.ts 中的声明

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.