@LihatChild di * ngIf

215

Pertanyaan

Apa cara paling elegan untuk mendapatkan @ViewChildsetelah elemen yang sesuai dalam template ditampilkan?

Di bawah ini adalah contohnya. Juga Plunker tersedia.

Templat:

<div id="layout" *ngIf="display">
    <div #contentPlaceholder></div>
</div>

Komponen:

export class AppComponent {

    display = false;
    @ViewChild('contentPlaceholder', {read: ViewContainerRef}) viewContainerRef;

    show() {
        this.display = true;
        console.log(this.viewContainerRef); // undefined
        setTimeout(()=> {
            console.log(this.viewContainerRef); // OK
        }, 1);
    }
}

Saya memiliki komponen dengan isinya disembunyikan secara default. Ketika seseorang memanggil show()metode itu menjadi terlihat. Namun, sebelum deteksi perubahan Angular 2 selesai, saya tidak bisa merujuk viewContainerRef. Saya biasanya memasukkan semua tindakan yang diperlukan setTimeout(()=>{},1)seperti yang ditunjukkan di atas. Apakah ada cara yang lebih benar?

Saya tahu ada opsi dengan ngAfterViewChecked, tetapi itu menyebabkan terlalu banyak panggilan tidak berguna.

JAWABAN (Plunker)

sinedsem
sumber
3
apakah Anda mencoba menggunakan atribut [tersembunyi] alih-alih * ngIf? Itu bekerja untuk saya untuk situasi yang sama.
Shardul

Jawaban:

335

Gunakan setter untuk ViewChild:

 private contentPlaceholder: ElementRef;

 @ViewChild('contentPlaceholder') set content(content: ElementRef) {
    if(content) { // initially setter gets called with undefined
        this.contentPlaceholder = content;
    }
 }

Setter disebut dengan referensi elemen begitu *ngIfmenjadi true.

Catatan, untuk Angular 8 Anda harus memastikan untuk mengatur { static: false }, yang merupakan pengaturan default di versi Agnular lainnya:

 @ViewChild('contentPlaceholder', { static: false })

Catatan: jika contentPlaceholder adalah komponen, Anda bisa mengubah ElementRef ke kelas komponen Anda:

  private contentPlaceholder: MyCustomComponent;
  @ViewChild('contentPlaceholder') set content(content: MyCustomComponent) {
     if(content) { // initially setter gets called with undefined
          this.contentPlaceholder = content;
     }
  }
parlemen
sumber
27
perhatikan bahwa penyetel ini disebut awalnya dengan konten yang tidak ditentukan, jadi periksa null jika melakukan sesuatu dalam penyetel
Recep
1
Baik jawabannya, tapi contentPlaceholderini ElementReftidak ViewContainerRef.
developer033
6
Bagaimana Anda memanggil setter?
Leandro Cusack
2
@LeandroCusack dipanggil secara otomatis ketika Angular menemukan <div #contentPlaceholder></div>. Secara teknis Anda dapat memanggilnya secara manual seperti setter lainnya this.content = someElementReftetapi saya tidak mengerti mengapa Anda ingin melakukan itu.
parlemen
3
Hanya catatan bermanfaat untuk siapa saja yang menemukan ini sekarang - Anda harus memiliki @ViewChild ('myComponent', {static: false}) di mana bit kunci adalah static: false, yang memungkinkannya untuk mengambil input yang berbeda.
nospamthanks
108

Alternatif untuk mengatasi ini adalah menjalankan detektor perubahan secara manual.

Anda pertama-tama menyuntikkan ChangeDetectorRef:

constructor(private changeDetector : ChangeDetectorRef) {}

Kemudian Anda menyebutnya setelah memperbarui variabel yang mengontrol * ngIf

show() {
        this.display = true;
        this.changeDetector.detectChanges();
    }
Jefferson Lima
sumber
1
Terima kasih! Saya menggunakan jawaban yang diterima tetapi masih menyebabkan kesalahan karena anak-anak masih tidak terdefinisi ketika saya mencoba menggunakannya beberapa waktu kemudian onInit(), jadi saya menambahkan detectChangessebelum memanggil fungsi anak dan memperbaikinya. (Saya menggunakan jawaban yang diterima dan jawaban ini)
Minyc510
Sangat membantu! Terima kasih!
AppDreamer
57

8 Sudut

Anda harus menambahkan { static: false }sebagai opsi kedua untuk @ViewChild. Ini menyebabkan hasil kueri diselesaikan setelah deteksi perubahan berjalan, memungkinkan Anda @ViewChilduntuk diperbarui setelah nilai berubah.

Contoh:

export class AppComponent {
    @ViewChild('contentPlaceholder', { static: false }) contentPlaceholder: ElementRef;

    display = false;

    constructor(private changeDetectorRef: ChangeDetectorRef) {
    }

    show() {
        this.display = true;
        this.changeDetectorRef.detectChanges(); // not required
        console.log(this.contentPlaceholder);
    }
}

Contoh Stackblitz: https://stackblitz.com/edit/angular-d8ezsn

Sviatoslav Oleksiv
sumber
3
Terima kasih Sviatoslav. Mencoba semuanya di atas tetapi hanya solusi Anda yang bekerja.
Peter Drinnan
Ini juga bekerja untuk saya (seperti trik viewchildren). Yang ini lebih intuitif dan lebih mudah untuk sudut 8.
Alex
2
Bekerja seperti pesona :)
Sandeep K Nair
1
Ini harus menjadi jawaban yang diterima untuk versi terbaru.
Krishna Prashatt
Saya menggunakan <mat-horizontal-stepper *ngIf="viewMode === DialogModeEnum['New']" linear #stepper, @ViewChild('stepper', {static: true}) private stepper: MatStepper;dan this.changeDetector.detectChanges();dan itu masih tidak berhasil.
Paul Strupeikis
21

Jawaban di atas tidak bekerja untuk saya karena dalam proyek saya, ngIf ada pada elemen input. Saya membutuhkan akses ke atribut nativeElement untuk fokus pada input ketika ngIf benar. Tampaknya tidak ada atribut nativeElement di ViewContainerRef. Inilah yang saya lakukan (mengikuti dokumentasi @ViewChild ):

<button (click)='showAsset()'>Add Asset</button>
<div *ngIf='showAssetInput'>
    <input #assetInput />
</div>

...

private assetInputElRef:ElementRef;
@ViewChild('assetInput') set assetInput(elRef: ElementRef) {
    this.assetInputElRef = elRef;
}

...

showAsset() {
    this.showAssetInput = true;
    setTimeout(() => { this.assetInputElRef.nativeElement.focus(); });
}

Saya menggunakan setTimeout sebelum fokus karena ViewChild membutuhkan beberapa detik untuk ditugaskan. Kalau tidak, itu tidak akan ditentukan.

zebraco
sumber
2
SetTimeout () dari 0 bekerja untuk saya. Elemen saya disembunyikan oleh ngIf saya terikat dengan benar setelah setTimeout, tanpa perlu fungsi set assetInput () di tengah.
Will Shaver
Anda dapat mendeteksi Perubahan di showAsset () dan tidak harus menggunakan batas waktu.
WrksOnMyMachine
Bagaimana ini jawabannya? OP sudah disebutkan menggunakan setTimeout? I usually wrap all required actions into setTimeout(()=>{},1) as shown above. Is there a more correct way?
Juan Mendes
12

Seperti yang disebutkan oleh yang lain, solusi tercepat dan tercepat adalah dengan menggunakan [disembunyikan] daripada * ngIf, dengan cara ini komponen akan dibuat tetapi tidak terlihat, karena Anda dapat memiliki akses ke sana, meskipun itu mungkin bukan yang paling efisien cara.

pengguna3728728
sumber
1
Anda harus perhatikan bahwa menggunakan "[tersembunyi]" mungkin tidak berfungsi jika elemennya bukan dari "display: block". lebih baik gunakan [style.display] = "condition? '': 'none'"
Félix Brunet
10

Ini bisa berhasil, tetapi saya tidak tahu apakah ini cocok untuk kasus Anda:

@ViewChildren('contentPlaceholder', {read: ViewContainerRef}) viewContainerRefs: QueryList;

ngAfterViewInit() {
 this.viewContainerRefs.changes.subscribe(item => {
   if(this.viewContainerRefs.toArray().length) {
     // shown
   }
 })
}
Günter Zöchbauer
sumber
1
Bisakah Anda mencoba ngAfterViewInit()alih-alih ngOnInit(). Saya berasumsi bahwa viewContainerRefsini sudah diinisialisasi tetapi belum mengandung item. Sepertinya saya ingat ini salah.
Günter Zöchbauer
Maaf aku salah. AfterViewInitsebenarnya bekerja. Saya telah menghapus semua komentar saya agar tidak membingungkan orang. Ini adalah Plunker yang berfungsi: plnkr.co/edit/myu7qXonmpA2hxxU3SLB?p=preview
sinedsem
1
Ini sebenarnya jawaban yang bagus. Ini berfungsi dan saya menggunakan ini sekarang. Terima kasih!
Konstantin
1
Ini bekerja untuk saya setelah upgrade dari sudut 7 ke 8. Untuk beberapa alasan, upgrade menyebabkan komponen tidak terdefinisi di afterViewInit bahkan dengan menggunakan static: false per sintaks ViewChild baru ketika komponen dibungkus dengan ngIf. Perhatikan juga bahwa QueryList memerlukan jenis sekarang seperti QueryList ini <YourComponentType>;
Alex
Mungkin perubahan yang terkait dengan constparameterViewChild
Günter Zöchbauer
9

"Trik" cepat lainnya (solusi mudah) adalah hanya menggunakan tag [tersembunyi] alih-alih * ngIf, hanya penting untuk mengetahui bahwa dalam kasus itu Angular membangun objek dan melukisnya di bawah kelas: disembunyikan inilah sebabnya ViewChild bekerja tanpa masalah . Jadi penting untuk diingat bahwa Anda tidak boleh menggunakan tersembunyi pada barang-barang berat atau mahal yang dapat menyebabkan masalah kinerja

  <div class="addTable" [hidden]="CONDITION">
Atau Yaacov
sumber
Jika yang tersembunyi ada di dalam yang lain jika kemudian perlu mengubah banyak hal
VIKAS KOHLI
6

Tujuan saya adalah untuk menghindari metode peretasan yang mengasumsikan sesuatu (misalnya setTimeout) dan saya akhirnya menerapkan solusi yang diterima dengan sedikit rasa RxJS di atas:

  private ngUnsubscribe = new Subject();
  private tabSetInitialized = new Subject();
  public tabSet: TabsetComponent;
  @ViewChild('tabSet') set setTabSet(tabset: TabsetComponent) {
    if (!!tabSet) {
      this.tabSet = tabSet;
      this.tabSetInitialized.next();
    }
  }

  ngOnInit() {
    combineLatest(
      this.route.queryParams,
      this.tabSetInitialized
    ).pipe(
      takeUntil(this.ngUnsubscribe)
    ).subscribe(([queryParams, isTabSetInitialized]) => {
      let tab = [undefined, 'translate', 'versions'].indexOf(queryParams['view']);
      this.tabSet.tabs[tab > -1 ? tab : 0].active = true;
    });
  }

Skenario saya: Saya ingin menjalankan aksi pada @ViewChildelemen tergantung pada router queryParams. Karena pembungkus *ngIfmenjadi salah sampai permintaan HTTP mengembalikan data, inisialisasi @ViewChildelemen terjadi dengan penundaan.

Bagaimana cara kerjanya: combineLatest memancarkan nilai untuk pertama kalinya hanya ketika masing-masing Observables yang disediakan memancarkan nilai pertama sejak saat combineLatestitu berlangganan. Subjek saya tabSetInitializedmemancarkan nilai saat @ViewChildelemen disetel. Dengan itu, saya menunda eksekusi kode di bawah subscribesampai *ngIfternyata positif dan @ViewChilddiinisialisasi.

Tentu saja jangan lupa berhenti berlangganan di ngOnDestroy, saya melakukannya menggunakan ngUnsubscribeSubjek:

  ngOnDestroy() {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }
Filip Juncu
sumber
terima kasih banyak saya sudah memiliki masalah yang sama, dengan tabSet & ngIf, metode Anda menghemat banyak waktu dan sakit kepala. Cheers m8;)
Exitl0l
3

Versi yang disederhanakan, saya memiliki masalah yang serupa dengan ini ketika menggunakan Google Maps JS SDK.

Solusi saya adalah untuk mengekstrak divdan ViewChildke dalam komponen anak itu sendiri yang ketika digunakan dalam komponen induk dapat disembunyikan / ditampilkan menggunakan *ngIf.

Sebelum

HomePageComponent Templat

<div *ngIf="showMap">
  <div #map id="map" class="map-container"></div>
</div>

HomePageComponent Komponen

@ViewChild('map') public mapElement: ElementRef; 

public ionViewDidLoad() {
    this.loadMap();
});

private loadMap() {

  const latLng = new google.maps.LatLng(-1234, 4567);
  const mapOptions = {
    center: latLng,
    zoom: 15,
    mapTypeId: google.maps.MapTypeId.ROADMAP,
  };
   this.map = new google.maps.Map(this.mapElement.nativeElement, mapOptions);
}

public toggleMap() {
  this.showMap = !this.showMap;
 }

Setelah

MapComponent Templat

 <div>
  <div #map id="map" class="map-container"></div>
</div>

MapComponent Komponen

@ViewChild('map') public mapElement: ElementRef; 

public ngOnInit() {
    this.loadMap();
});

private loadMap() {

  const latLng = new google.maps.LatLng(-1234, 4567);
  const mapOptions = {
    center: latLng,
    zoom: 15,
    mapTypeId: google.maps.MapTypeId.ROADMAP,
  };
   this.map = new google.maps.Map(this.mapElement.nativeElement, mapOptions);
}

HomePageComponent Templat

<map *ngIf="showMap"></map>

HomePageComponent Komponen

public toggleMap() {
  this.showMap = !this.showMap;
 }
Eugene
sumber
1

Dalam kasus saya, saya perlu memuat seluruh modul hanya ketika div ada dalam template, artinya outlet berada di dalam sebuah ngif. Dengan cara ini, setiap sudut mendeteksi elemen #geolocalisationOutlet, ia membuat komponen di dalamnya. Modul ini hanya memuat sekali juga.

constructor(
    public wlService: WhitelabelService,
    public lmService: LeftMenuService,
    private loader: NgModuleFactoryLoader,
    private injector: Injector
) {
}

@ViewChild('geolocalisationOutlet', {read: ViewContainerRef}) set geolocalisation(geolocalisationOutlet: ViewContainerRef) {
    const path = 'src/app/components/engine/sections/geolocalisation/geolocalisation.module#GeolocalisationModule';
    this.loader.load(path).then((moduleFactory: NgModuleFactory<any>) => {
        const moduleRef = moduleFactory.create(this.injector);
        const compFactory = moduleRef.componentFactoryResolver
            .resolveComponentFactory(GeolocalisationComponent);
        if (geolocalisationOutlet && geolocalisationOutlet.length === 0) {
            geolocalisationOutlet.createComponent(compFactory);
        }
    });
}

<div *ngIf="section === 'geolocalisation'" id="geolocalisation">
     <div #geolocalisationOutlet></div>
</div>
Gabb1995
sumber
1

Saya pikir menggunakan penundaan dari lodash sangat masuk akal terutama dalam kasus saya di mana saya @ViewChild()berada di dalam asyncpipa

Timo
sumber
0

Bekerja pada Angular 8 Tidak perlu mengimpor ChangeDector

ngIf memungkinkan Anda untuk tidak memuat elemen dan menghindari menambahkan lebih banyak tekanan ke aplikasi Anda. Inilah cara saya menjalankannya tanpa ChangeDetector

elem: ElementRef;

@ViewChild('elemOnHTML', {static: false}) set elemOnHTML(elemOnHTML: ElementRef) {
    if (!!elemOnHTML) {
      this.elem = elemOnHTML;
    }
}

Kemudian ketika saya mengubah nilai ngIf saya menjadi benar, saya akan menggunakan setTimeout seperti ini untuk menunggu hanya untuk siklus perubahan berikutnya:

  this.showElem = true;
  console.log(this.elem); // undefined here
  setTimeout(() => {
    console.log(this.elem); // back here through ViewChild set
    this.elem.do();
  });

Ini juga memungkinkan saya untuk menghindari menggunakan perpustakaan atau impor tambahan.

Manuel BM
sumber
0

untuk Angular 8 - campuran pemeriksaan nol dan @ViewChild static: falseperetasan

untuk kontrol paging menunggu data async

@ViewChild(MatPaginator, { static: false }) set paginator(paginator: MatPaginator) {
  if(!paginator) return;
  paginator.page.pipe(untilDestroyed(this)).subscribe(pageEvent => {
    const updated: TSearchRequest = {
      pageRef: pageEvent.pageIndex,
      pageSize: pageEvent.pageSize
    } as any;
    this.dataGridStateService.alterSearchRequest(updated);
  });
}
jenson-button-event
sumber
0

Ini berfungsi untuk saya jika saya menggunakan ChangeDetectorRef di Angular 9

@ViewChild('search', {static: false})
public searchElementRef: ElementRef;

constructor(private changeDetector: ChangeDetectorRef) {}

//then call this when this.display = true;
show() {
   this.display = true;
   this.changeDetector.detectChanges();
}
Ivan Sim
sumber