Anotasi 2 sudut pandang @ViewChild mengembalikan yang tidak ditentukan

242

Saya mencoba belajar Angular 2.

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

Berikut beberapa baris kode:

Di BodyContent.ts saya punya:

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

saat 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 templat (seperti yang disarankan dalam 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>

Templat FilterTiles.html dimuat dengan benar ke tag ico-filter-tiles (memang saya bisa melihat tajuknya).

Catatan: kelas BodyContent disuntikkan ke dalam templat lain (Tubuh) 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 ftke log konsol, saya dapat undefined, dan tentu saja saya mendapatkan pengecualian ketika saya mencoba untuk mendorong sesuatu di dalam array "ubin": 'tidak ada ubin properti untuk "tidak terdefinisi" .

Satu hal lagi: Komponen FilterTiles tampaknya dimuat dengan benar, karena saya dapat melihat templat html untuk itu.

Ada saran? Terima kasih

Andrea Ialenti
sumber
Terlihat benar. Mungkin sesuatu dengan templat, tetapi tidak termasuk dalam pertanyaan Anda.
Günter Zöchbauer
1
Setuju dengan Günter. Saya membuat plunkr dengan kode Anda dan templat sederhana yang terkait dan berfungsi. Lihat tautan ini: plnkr.co/edit/KpHp5Dlmppzo1LXcutPV?p=preview . Kami membutuhkan templat ;-)
Thierry Templier
1
fttidak akan diatur dalam konstruktor, tetapi dalam event handler klik itu akan sudah ditetapkan.
Günter Zöchbauer
5
Anda menggunakan loadAsRoot, yang memiliki masalah yang diketahui dengan deteksi perubahan. Hanya untuk memastikan coba gunakan loadNextToLocationatau loadIntoLocation.
Eric Martinez
1
Masalahnya adalah loadAsRoot. Setelah saya diganti dengan loadIntoLocationmasalah terpecahkan. Jika Anda membuat komentar sebagai jawaban, saya dapat menandainya sebagai diterima
Andrea Ialenti

Jawaban:

374

Saya memiliki masalah yang sama dan berpikir saya akan memposting kalau-kalau ada orang yang membuat kesalahan yang sama. Pertama, satu hal yang perlu dipertimbangkan adalah AfterViewInit; Anda harus menunggu tampilan diinisialisasi sebelum Anda dapat mengakses @ViewChild. Namun, saya @ViewChildmasih mengembalikan nol. Masalahnya adalah masalah saya *ngIf. The *ngIfdirektif membunuh saya komponen kontrol jadi saya tidak bisa referensi itu.

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

Semoga itu bisa membantu.

EDIT
Seperti yang disebutkan oleh @Ashg di bawah ini , solusinya adalah menggunakan @ViewChildrenalih-alih @ViewChild.

kenecaswell
sumber
9
@kenecaswell Jadi, apakah Anda menemukan cara yang lebih baik untuk menyelesaikan masalah. Saya juga menghadapi masalah yang sama. Saya punya banyak * ngIf sehingga elemen hanya akan menjadi benar, tetapi saya perlu referensi elemen. Cara apa pun untuk menyelesaikan ini>
monica
4
Saya menemukan bahwa komponen turunan adalah 'tidak terdefinisi' di ngAfterViewInit () jika menggunakan ngIf. Saya mencoba meletakkan batas waktu yang lama tetapi tetap tidak berpengaruh. Namun, komponen anak tersedia nanti (yaitu sebagai respons terhadap peristiwa klik, dll.). Jika saya tidak menggunakan ngIf dan didefinisikan seperti yang diharapkan di ngAfterViewInit (). Ada lebih banyak tentang komunikasi Orangtua / Anak di sini angular.io/docs/ts/latest/cookbook/…
Matthew Hegarty
3
Saya menggunakan bootstrap ngClass+ hiddenclass sebagai ganti ngIf. Itu berhasil. Terima kasih!
Rahmathullah M
9
Ini tidak menyelesaikan masalah, gunakan solusi di bawah ini menggunakan @ViewChildren, dapatkan referensi ke kontrol anak setelah tersedia
Ashg
20
Ini hanya membuktikan "masalah", kan? Itu tidak memposting solusi.
Miguel Ribeiro
146

Masalah seperti yang disebutkan sebelumnya adalah ngIfyang menyebabkan pandangan menjadi tidak terdefinisi. Jawabannya adalah menggunakan ViewChildrenalih-alih ViewChild. Saya memiliki masalah serupa di mana saya tidak ingin grid ditampilkan sampai semua data referensi telah 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 kami menggunakan ViewChildrendi mana Anda dapat mendengarkan perubahan. Dalam hal ini ada anak dengan referensi #searchGrid. Semoga ini membantu.

Ashg
sumber
4
Saya ingin menambahkan itu dalam beberapa kasus ketika Anda mencoba mengubah mis. this.SearchGridproperti Anda harus menggunakan sintaks ingin setTimeout(()=>{ ///your code here }, 1); menghindari Pengecualian: Ekspresi telah berubah setelah diperiksa
rafalkasa
3
Bagaimana Anda melakukan ini jika Anda ingin menempatkan tag #searchGrid Anda pada elemen HTML normal alih-alih elemen Angular2? (Misalnya, <div #searchGrid> </div> dan ini ada di dalam blok * ngIf?
Vern Jensen
1
ini jawaban yang benar untuk kasus penggunaan saya! Terima kasih, saya perlu mengakses komponen yang tersedia melalui ngIf =
Frozen_byte
1
ini berfungsi sempurna pada respons ajax, sekarang *ngIfberfungsi, dan setelah render kita dapat menyimpan ElementRef dari komponen dinamis.
elporfirio
4
Juga jangan lupa untuk menetapkannya untuk berlangganan dan kemudian berhenti berlangganan
tam.teixeira
64

Anda dapat menggunakan setter untuk @ViewChild()

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

Jika Anda memiliki pembungkus ngIf, penyetel akan dipanggil dengan tidak terdefinisi, dan sekali lagi dengan referensi begitu ngIf memungkinkannya merender.

Masalah saya adalah sesuatu yang lain. Saya belum memasukkan modul yang berisi "FilterTiles" saya di app.modules saya. Templat tidak melempar kesalahan tetapi referensi selalu tidak ditentukan.

parlemen
sumber
3
Ini tidak berfungsi untuk saya - saya mendapatkan yang pertama tidak terdefinisi, tetapi saya tidak mendapatkan panggilan kedua dengan referensi. App adalah ng2 ... apakah ini fitur ng4 +?
Jay Cummins
@ Jay Saya percaya ini karena Anda belum mendaftarkan komponen dengan Angular, dalam hal ini FilterTiles. Saya pernah mengalami masalah itu karena alasan itu sebelumnya.
parlemen
1
Bekerja untuk Angular 8 menggunakan #paginator pada elemen html dan anotasi seperti@ViewChild('paginator', {static: false})
Qiteq
1
Apakah ini panggilan balik untuk perubahan ViewChild?
Yasser Nascimento
24

Ini berhasil untuk saya.

Komponen saya bernama 'komponen saya', misalnya, ditampilkan menggunakan * ngIf = "showMe" seperti:

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

Jadi, ketika komponen diinisialisasi komponen belum ditampilkan sampai "showMe" benar. Jadi, referensi @ViewChild saya semuanya tidak terdefinisi.

Di sinilah saya menggunakan @ViewChildren dan QueryList yang dikembalikan. Lihat artikel sudut pada QueryList dan demo penggunaan @ViewChildren .

Anda dapat menggunakan QueryList yang dikembalikan @ViewChildren dan berlangganan setiap perubahan 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 bisa membantu seseorang menghemat waktu dan frustrasi.

Joshua Dyck
sumber
3
Memang solusi Anda tampaknya menjadi pendekatan terbaik yang terdaftar sejauh ini. CATATAN Kita harus ingat bahwa solusi 73 teratas sekarang DIHAPUSKAN ... karena arahan: [...] deklarasi tidak lagi didukung dalam Angular 4. TUNGGU TIDAK AKAN bekerja dalam skenario Angular 4
PeteZaria
5
Jangan lupa untuk berhenti berlangganan, atau gunakan .take(1).subscribe(), tetapi jawaban yang bagus, terima kasih banyak!
Blair Connolly
2
Solusi yang sangat baik. Saya telah berlangganan perubahan ref di ngAfterViewInit () daripada ngOnChanges (). Tapi saya harus menambahkan setTimeout untuk menghilangkan kesalahan ExpressionChangedAfterChecked
Josf
Ini harus ditandai sebagai solusi aktual. Terima kasih banyak!
platzhersh
10

Solusi saya adalah menggunakan [style.display]="getControlsOnStyleDisplay()"alih-alih *ngIf="controlsOn". Blok ada 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";
    }
  }
....
Calderas
sumber
Memiliki halaman di mana daftar item ditampilkan dalam tabel, atau item edit ditampilkan, berdasarkan nilai variabel showList. Menyingkirkan kesalahan konsol yang mengganggu dengan menggunakan [style.display] = "! ShowList", dikombinasikan dengan * ngIf = "! ShowList".
razvanone
9

Yang memecahkan masalah saya adalah memastikan staticdiatur ke false.

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

Dengan staticdimatikan, @ViewChildreferensi diperbarui oleh Angular ketika *ngIfdirektif berubah.

Paul LeBeau
sumber
1
Ini hampir merupakan penjawab sempurna, hanya untuk menunjuk adalah praktik yang baik untuk juga memeriksa nilai nullable, jadi kita berakhir dengan sesuatu seperti ini: @ViewChild (ClrForm, {static: false}) set clrForm (clrForm: ClrForm) {if (clrForm) {this.clrForm = clrForm; }};
Klaus Klein
Saya mencoba banyak hal dan akhirnya menemukan hal ini menjadi pelakunya.
Manish Sharma
8

Solusi saya adalah menggantinya *ngIf dengan [hidden]. Kelemahannya adalah semua komponen anak hadir dalam kode DOM. Tetapi bekerja untuk kebutuhan saya.

suzanne suzanne
sumber
5

Dalam kasus saya, saya mempunyai setter variabel input menggunakan ViewChild, dan bagian ViewChilddalam *ngIfdirektif, sehingga setter mencoba mengaksesnya sebelum *ngIfdiberikan (itu akan berfungsi dengan baik tanpa *ngIf, tetapi tidak akan berfungsi jika selalu diatur ke benar dengan*ngIf="true" ).

Untuk menyelesaikannya, saya menggunakan Rxjs untuk memastikan referensi apa pun yang ViewChildmenunggu hingga tampilan dimulai. Pertama, buat Subjek yang selesai ketika setelah view init.

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

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

Kemudian, buat fungsi yang mengambil dan menjalankan 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;
nikojpapa
sumber
1
Ini adalah solusi yang jauh lebih baik daripada semua penyelesaian nonesense
Liam
4

Itu harus bekerja.

Tapi seperti yang dikatakan Günter Zöchbauer pasti ada masalah lain dalam template. Saya telah membuat agak Relevant-Plunkr-Answer . Tolong jangan periksa konsol browser.

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

Itu bekerja seperti pesona. Harap periksa tag dan referensi Anda.

Terima kasih...

mikronik
sumber
1
Jika masalahnya sama dengan saya, untuk menduplikasi Anda harus meletakkan * ngIf di templat di sekitar <filter> </filter>. Rupanya jika ngIf mengembalikan false, ViewChild tidak terhubung dengan kabel dan mengembalikan null
Dan Mengejar
2

Ini berfungsi untuk saya, lihat contoh di bawah ini.

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

}

NiZa
sumber
2

Saya memiliki masalah serupa, di mana ViewChildada di dalam switchklausa yang tidak memuat elemen viewChild sebelum direferensikan. Saya memecahkannya dengan cara semi-hacky tetapi membungkus ViewChildreferensi dengan setTimeoutyang dieksekusi segera (yaitu 0ms)

Nikola Jankovic
sumber
1

Solusi saya untuk ini adalah memindahkan ngIf dari luar komponen anak ke dalam komponen anak pada div yang membungkus seluruh bagian html. Dengan cara itu masih tersembunyi ketika perlu, tetapi mampu memuat komponen dan saya bisa referensi di induknya.

harmonickey
sumber
Tetapi untuk itu, bagaimana Anda mendapatkan variabel "terlihat" yang ada di induk?
Dan Chase
1

Saya memperbaikinya hanya menambahkan SetTimeout setelah mengatur komponen yang terlihat

HTML saya:

<input #txtBus *ngIf[show]>

Komponen JS saya

@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);
  }
}
Osvaldo Leiva
sumber
1

Dalam kasus saya, saya tahu komponen anak akan selalu ada, tetapi ingin mengubah keadaan sebelum menginisialisasi anak untuk menyelamatkan pekerjaan.

Saya memilih untuk menguji untuk anak sampai muncul dan membuat perubahan segera, yang menyelamatkan saya siklus perubahan pada komponen anak.

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

Secara peringatan, Anda dapat menambahkan jumlah upaya maksimal atau menambahkan beberapa penundaan ms ke setTimeout. setTimeoutsecara efektif melemparkan fungsi ke bagian bawah daftar operasi yang tertunda.

Tidak makan
sumber
0

Semacam pendekatan umum:

Anda dapat membuat metode yang akan menunggu hingga ViewChildsiap

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

Pemakaian:

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

Bagi saya masalahnya adalah saya mereferensikan ID pada elemen.

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

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

Alih-alih seperti ini:

@ViewChild('surveyForm') slides:IonSlides;

<div #surveyForm></div>
Jeremy Cepat
sumber
0

Jika Anda menggunakan Ionic, Anda harus menggunakan ionViewDidEnter()kait siklus hidup. Ionik berjalan beberapa hal tambahan (terutama animasi-terkait) yang biasanya menyebabkan kesalahan tak terduga seperti ini, maka kebutuhan untuk sesuatu yang berjalan setelah ngOnInit , ngAfterContentInitdan sebagainya.

Jai
sumber
-1

Inilah sesuatu yang berhasil untuk saya.

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

Jadi saya pada dasarnya menetapkan cek setiap detik sampai *ngIfmenjadi benar dan kemudian saya melakukan hal-hal yang berkaitan dengan saya ElementRef.

Anjil Dhamala
sumber
-3

Solusi yang berhasil bagi saya adalah menambahkan arahan dalam deklarasi di app.module.ts

noro5
sumber