繁体   English   中英

Angular 2 @ViewChild 注释返回未定义

[英]Angular 2 @ViewChild annotation returns undefined

我正在尝试学习 Angular 2。

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

这里有几行代码:

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

最后是模板(如评论中所建议):

正文内容.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标记中(确实我能够看到标题)。

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

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 property tiles for "undefined"'

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

有什么建议么?

我有一个类似的问题,并认为我会发布以防其他人犯同样的错误。 首先,要考虑的一件事是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

前面提到的问题是导致视图未定义的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的孩子。 希望这可以帮助。

您可以为@ViewChild()使用 setter

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

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

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

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

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

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

这对我有用。

例如,我的组件名为 'my-component' 使用 *ngIf="showMe" 显示,如下所示:

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

因此,当组件初始化时,组件在“showMe”为真之前不会显示。 因此,我的 @ViewChild 引用都是未定义的。

这是我使用@ViewChildren 和它返回的QueryList 的地方。 请参阅有关 QueryList 的 Angular 文章和 @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

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

我对此的解决方案是将*ngIf替换为[hidden] 缺点是所有子组件都存在于代码 DOM 中。 但为我的要求工作。

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

就我而言,我有一个使用ViewChild的输入变量设置器,并且ViewChild位于*ngIf指令内,因此设置器试图在*ngIf呈现之前访问它(如果没有*ngIf它将正常工作,但会如果它始终使用*ngIf="true"设置为 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;

它必须工作。

但正如Günter Zöchbauer所说,模板中肯定还有其他问题。 我创建了有点Relevant-Plunkr-Answer 请检查浏览器的控制台。

引导.ts

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

过滤器Tiles.ts

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


 export class FilterTiles {
     public tiles = [];

     public constructor(){};
 }

它就像一个魅力。 请仔细检查您的标签和参考资料。

谢谢...

对我来说,使用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
  }
}

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

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

 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引用包装在立即执行的setTimeout中(即 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
  })

如果 *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>

只需将 {static: true} 添加到 @View 即可解决我的问题。

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

我修复它只是在设置可见组件后添加 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));
    }
}

Alertnatively,您可以添加最大尝试次数或向setTimeout添加几毫秒的延迟。 setTimeout有效地将函数抛出到挂起操作列表的底部。

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

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

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

而不是这样:

@ViewChild('surveyForm') slides:IonSlides;

<div #surveyForm></div>

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

对于 Angular:在 HTML 中使用显示样式“block”或“none”更改 *ngIf。

selector: 'app',
template:  `
    <controls [style.display]="controlsOn ? 'block' : 'none'"></controls>
    <slideshow (mousemove)="onMouseMove()"></slideshow>
`,
directives: [SlideshowComponent, ControlsComponent]

这是对我有用的东西。

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

我有一个类似的问题,其中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获取引用并继续进行逻辑。

除了其他答案之外,您还可以使用最后一个生命周期挂钩:

ngAfterViewChecked() {}

即使在ngAfterViewChecked之后也会调用ngAfterViewInit

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM