首页 文章

Angular 2 @ViewChild注释返回undefined

提问于
浏览
129

我正在努力学习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 标签中(实际上我能够看到 Headers ) .

注意:BodyContent类使用DynamicComponetLoader注入另一个模板(Body):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 368579 ' .

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

有什么建议吗?谢谢

13 回答

  • 215

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

    希望有所帮助 .

    EDIT
    如下面@Ashg所述,解决方案是使用 @ViewChildren 而不是 @ViewChild .

  • 1

    In my case, I knew the child component would always be present, but wanted to alter the state prior to the child initializing to save work.

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

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

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

  • 15

    它必须工作 .

    但正如 Günter Zöchbauer 所说,模板中一定存在其他一些问题 . 我创造了有点Relevant-Plunkr-Answer . 请检查浏览器的控制台 .

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

    filterTiles.ts

    @Component({
         selector: 'filter',
        template: '<div> <h4>Filter tiles </h4></div>'
     })
    
    
     export class FilterTiles {
         public tiles = [];
    
         public constructor(){};
     }
    

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

    谢谢...

  • 3

    这适用于我,请参阅下面的示例 .

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

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

  • 3

    我有一个类似的问题, ViewChild 在一个 switch 子句内部,在它被引用之前没有加载viewChild元素 . 我以一种半hacky的方式解决了它,但将 ViewChild 引用包含在立即执行的_368610中(即0ms)

  • 1

    您可以使用 @ViewChild() 的setter

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

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

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

  • 85

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

    我的解决方案是用[hidden]替换* ngIf . 下行是代码DOM中存在的所有子组件 . 但是按照我的要求工作 .

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

    在我的例子中,我有一个使用ViewChild的输入变量setter,而ViewChild在* ngIf指令中,所以setter试图在* ngIf渲染之前访问它(它会在没有* ngIf的情况下正常工作,但是如果* ngIf =“true”总是设置为true,则不起作用 .

    为了解决这个问题,我使用了Rxjs来确保对ViewChild的任何引用都等到视图启动 . 首先,创建一个在查看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;
    
  • 39

    这对我有用 .

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

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

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

    这是我使用@ViewChildren和它返回的QueryList的地方 . 见angular article on QueryList and a @ViewChildren usage demo .

    您可以使用@ViewChildren返回的QueryList,并使用rxjs订阅对引用项的任何更改,如下所示 . @ViewChid没有这种能力 .

    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
    

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

  • 2

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

    html:

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

    Component Code

    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引用的子节点 . 希望这可以帮助 .

相关问题