Angular 2 @ViewChild注释返回undefined

我正在学习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: dcl.loadAsRoot(BodyContent, '#ico-bodyContent', injector)注入到另一个模板(Body)中:

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”时,我会得到一个异常。数组:“未定义”没有属性瓷砖

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

有什么建议吗?

397306 次浏览

它必须起作用。

但正如< em > Gunter Zochbauer < / em >所说,模板中一定有其他问题。我已经创建了Relevant-Plunkr-Answer。请检查浏览器控制台。

< em > boot.ts < / em >

@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");
}
}

< em > filterTiles.ts < / em >

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




export class FilterTiles {
public tiles = [];


public constructor(){};
}

这招很管用。请仔细检查您的标签和参考资料。

谢谢……

我也遇到过类似的问题,我想我应该发布这篇文章,以防别人犯同样的错误。首先,要考虑的一件事是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
}
}

希望这能有所帮助。

< p > 编辑
正如@Ashg下面所提到的,一个解决方案是使用@ViewChildren而不是@ViewChild.

前面提到的问题是导致视图未定义的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的子元素。希望这能有所帮助。

这对我很管用。

例如,我的组件名为“My -component”,使用*ngIf="showMe"显示。 像这样:< / p >
<my-component [showMe]="showMe" *ngIf="showMe"></my-component>

因此,当组件初始化时,直到"showMe"为真,组件才会显示出来。因此,我的@ViewChild引用都没有定义。

这是我使用@ViewChildren和它返回的QueryList的地方。看到angular的文章在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

希望这能帮助一些人节省一些时间和挫折。

我的解决方法是使用[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";
}
}
....

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

这对我来说是可行的,参见下面的示例。

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(); });
}
}


}

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

你可以为@ViewChild()使用setter

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

如果你有一个ngIf包装器,setter将被undefined调用,然后在ngIf允许它呈现时再次被引用调用。

但我的问题是另一回事。我没有在我的app.modules中包含我的“FilterTiles”模块。模板没有抛出错误,但是引用总是未定义的。

我的解决方案是用[hidden]替换*ngIf。缺点是所有子组件都出现在代码DOM中。但满足了我的要求。

我修复它只是添加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);
}
}

在我的例子中,我知道子组件将始终存在,但希望在初始化子组件之前更改状态以节省工作。

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

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));
}
}

注意,您可以添加一个最大尝试次数或添加一些ms延迟到setTimeoutsetTimeout有效地将函数抛出到挂起操作列表的底部。

在我的例子中,我有一个使用ViewChild的输入变量setter,而ViewChild位于*ngIf指令内部,因此setter试图在*ngIf呈现之前访问它(没有*ngIf它可以正常工作,但如果它总是被*ngIf="true"设置为true,则无法正常工作)。

为了解决这个问题,我使用Rxjs来确保任何对ViewChild的引用都要等到视图被初始化。首先,创建一个在view init之后完成的Subject。

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;

一种通用的方法:

您可以创建一个方法,该方法将等待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
})

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

这里有一些对我有用的东西。

@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相关的事情。

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

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


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

而不是这样:

@ViewChild('surveyForm') slides:IonSlides;


<div #surveyForm></div>

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

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

关闭static后,当*ngIf指令改变时,Angular会更新@ViewChild引用。

如果你正在使用Ionic,你将需要使用ionViewDidEnter()生命周期钩子。Ionic运行一些额外的东西(主要是与动画相关的),通常会导致像这样的意外错误,因此需要运行 ngOnInitngAfterContentInit等的东西。

对我来说,使用ngAfterViewInit而不是ngOnInit修复了这个问题:

export class AppComponent implements OnInit {
@ViewChild('video') video;
ngOnInit(){
// <-- in here video is undefined
}
public ngAfterViewInit()
{
console.log(this.video.nativeElement) // <-- you can access it here
}
}
< p >角: 在HTML中更改*ngIf的显示样式为'block'或'none'
selector: 'app',
template:  `
<controls [style.display]="controlsOn ? 'block' : 'none'"></controls>
<slideshow (mousemove)="onMouseMove()"></slideshow>
`,
directives: [SlideshowComponent, ControlsComponent]

如果*ngIf="show"防止ViewChild被渲染,并且你需要在你的show变为true后立即使用ViewChild,它帮助我在设置show为true后立即触发ChangeDetectorRef.detectChanges()。

在此之后,*ngIf创建组件并渲染ViewChild,你可以在之后使用它。只是输入了一个快速的示例代码。

@ViewChild(MatSort) sort: MatSort;


constructor(private cdRef: ChangeDetectorRef) {}


ngOnInit() {
this.show = false;
this.someObservable()
.pipe(
tap(() => {
this.show = true;
this.cdRef.detectChanges();
})
)
.subscribe({
next: (data) => {
console.log(sort)
this.useResult(data);
}
});
}

这是坏事吗?还是为什么没有人提出?

使用[hidden]代替*ngif,因为*ngif在条件不满足时终止代码。

<div [hidden]="YourVariable">
Show Something
</div>

我有一个类似的问题,其中ViewChild在有条件(*ngIf)呈现的组件中。它将在api调用的响应上呈现。该响应晚于@ViewChild装饰器执行时,因此所需的组件引用保持未定义(null)。在使用{static: false}后,即使所需的组件在一段时间(一小段时间)后可见,@ViewChild装饰器也不会再次触发。这违背了Angular的“承诺”😢(在本帖子的其他回答中已经说明了)

原因是ChangeDetectionStrategy 设置为 OnPush😧。当将其更改为ChangeDetectionStrategy.Default时,一切正常工作。

结论:

  1. ✅使用{ static: false } &
  2. ChangeDetectionStrategy.Default

用于有条件(*ngIf)呈现的@ViewChild组件,以便“稍后”获得它们的引用;(当它们被渲染时)

我借助更改检测以及视图容器引用的延迟初始化解决了这个问题。

HTML设置:

<ng-container *ngIf="renderMode === 'modal'" [ngTemplateOutlet]="renderModal">
</ng-container>
<ng-container *ngIf="renderMode === 'alert'" [ngTemplateOutlet]="renderAlert">
</ng-container>


<ng-template #renderModal>
<div class="modal">
<ng-container appSelector></ng-container>
</div>
</ng-template>


<ng-template #renderAlert>
<div class="alert">
<ng-container appSelector></ng-container>
</div>
</ng-template>

组件:

@ViewChild(SelectorDirective, { static: true }) containerSelector!: SelectorDirective;


constructor(private cdr: ChangeDetectorRef) { }


ngOnInit(): void {
// step: 1
this.renderMode = someService.someMethod();
// step: 2
this.cdr.markForCheck();
// step: 3
const viewContainerRef = this.containerSelector?.viewContainerRef;
if (viewContainerRef) {
// logic...
}
}
  1. 修改了代码,使HTML所依赖的条件(*ngIf)应该首先更新
  2. 一旦条件更新,手动触发ChangeDetection
  3. 在手动cdr触发后从ViewChild获取引用,并继续进行逻辑操作。

只是添加{static: true} @View解决了我的问题。

@ViewChild(FilterTiles, { static : true }) ft: FilterTiles;

除了其他答案,你还可以使用最后一个生命周期钩子:

ngAfterViewChecked() {}

即使在ngAfterViewInit之后也会调用ngAfterViewChecked

生命周期钩子:https://angular.io/guide/lifecycle-hooks#lifecycle-event-sequence