typescript - anotasi Angular 2 @ViewChild mengembalikan undefined


Saya sedang mencoba belajar Angular 2.

Saya ingin mengakses komponen anak dari komponen induk menggunakan Anotasi @ViewChild.

Berikut beberapa baris kode:

Di BodyContent.ts saya memiliki:

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

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

Akhirnya di sini templatenya (seperti yang disarankan di komentar):

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>

Template FilterTiles.html dimuat dengan benar ke dalam tag ico-filter-tiles (memang saya bisa melihat headernya).

Catatan: kelas BodyContent disuntikkan di dalam template lain (Body) menggunakan 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);
   } 
}

Masalahnya adalah ketika saya mencoba menulis ft ke dalam log konsol, saya mendapatkan undefined, dan tentu saja saya mendapatkan pengecualian ketika mencoba menambahkan sesuatu ke dalam array "tiles": 'tidak ada properti tiles untuk "undefined"'.

Satu hal lagi: komponen FilterTiles tampaknya dimuat dengan benar, karena saya bisa melihat template HTML untuknya.

Ada saran?

Jawaban #1

Saya mengalami masalah serupa dan berpikir untuk mempostingnya jika ada orang lain yang membuat kesalahan yang sama. Pertama, satu hal yang perlu dipertimbangkan adalah AfterViewInit; Anda perlu menunggu tampilan diinisialisasi sebelum dapat mengakses @ViewChild Anda. Namun, @ViewChild saya masih mengembalikan null. Masalahnya adalah *ngIf saya. Direktif *ngIf menghapus komponen kontrol saya sehingga saya tidak bisa mereferensinya.

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
Seperti yang disebutkan oleh @Ashg di bawah, solusi adalah menggunakan @ViewChildren daripada @ViewChild.

Jawaban #2

Masalah seperti yang disebutkan sebelumnya adalah ngIf yang menyebabkan tampilan menjadi tidak terdefinisi. Solusinya adalah menggunakan ViewChildren alih-alih ViewChild. Saya memiliki masalah serupa di mana saya tidak ingin grid ditampilkan sampai semua data referensi dimuat.

html:

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

Kode Komponen

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

       
    }
}

Di sini kita menggunakan ViewChildren yang dapat Anda dengarkan perubahannya. Dalam hal ini, setiap anak dengan referensi #searchGrid.

Jawaban #3

Anda bisa menggunakan setter untuk @ViewChild()

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

Jika Anda memiliki pembungkus ngIf, setter akan dipanggil dengan undefined, dan kemudian lagi dengan referensi setelah ngIf memungkinkan untuk merender.

Masalah saya sebenarnya sesuatu yang lain. Saya belum memasukkan modul yang berisi 'FilterTiles' dalam app.modules saya. Templatenya tidak menampilkan error tetapi referensinya selalu tidak terdefinisi.

Jawaban #4

Yang menyelesaikan masalah saya adalah memastikan static disetel ke false.

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

Dengan static dimatikan, referensi @ViewChild diperbarui oleh Angular ketika direktif *ngIf berubah.

Jawaban #5

Ini berhasil untuk saya.

Komponen saya yang bernama 'my-component', misalnya, ditampilkan menggunakan *ngIf="showMe" seperti ini:

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

Jadi, ketika komponen diinisialisasi, komponen tersebut belum ditampilkan sampai "showMe" bernilai true. Oleh karena itu, semua referensi @ViewChild saya semuanya tidak terdefinisi.

Di sinilah saya menggunakan @ViewChildren dan QueryList yang dikembalikannya. Lihat artikel Angular tentang QueryList dan demo penggunaan @ViewChildren.

Anda dapat menggunakan QueryList yang dikembalikan oleh @ViewChildren dan berlangganan terhadap perubahan apa pun pada item yang direferensikan menggunakan rxjs seperti yang terlihat di bawah ini. @ViewChild tidak memiliki kemampuan ini.

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

Semoga ini membantu seseorang menghemat waktu dan mengurangi frustrasi.

Jawaban #6

Solusi saya untuk ini adalah mengganti *ngIf dengan [hidden]. Kekurangannya adalah semua komponen anak tetap ada di DOM kode. Tapi ini bekerja untuk kebutuhan saya.

Jawaban #7

Dalam kasus saya, saya memiliki setter variabel input yang menggunakan ViewChild, dan ViewChild itu berada di dalam direktif *ngIf, jadi setter mencoba mengaksesnya sebelum *ngIf dirender (ini akan bekerja dengan baik tanpa *ngIf, tetapi tidak akan bekerja jika selalu diatur ke true dengan *ngIf="true").

Untuk menyelesaikan, saya menggunakan Rxjs untuk memastikan setiap referensi ke ViewChild menunggu sampai tampilan diinisialisasi. Pertama, buat sebuah Subject yang selesai setelah inisialisasi tampilan.

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

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

Kemudian, buatlah sebuah fungsi yang mengambil dan mengeksekusi sebuah lambda setelah subjek selesai.

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

Akhirnya, pastikan referensi ke ViewChild menggunakan fungsi ini.

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

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

Jawaban #8

Solusi sementara saya adalah menggunakan [style.display]="getControlsOnStyleDisplay()" alih-alih *ngIf="controlsOn". Bloknya ada di sana tetapi tidak ditampilkan.

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

Artikel terkait