@ViewChild in *ngIf

AngularAngular2 ChangedetectionViewchild

Angular Problem Overview


Question

What is the most elegant way to get @ViewChild after corresponding element in template was shown?

Below is an example. Also Plunker available.

Component.template.html:

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

Component.component.ts:

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

I have a component with its contents hidden by default. When someone calls show() method it becomes visible. However, before Angular 2 change detection completes, I can not reference to viewContainerRef. I usually wrap all required actions into setTimeout(()=>{},1) as shown above. Is there a more correct way?

I know there is an option with ngAfterViewChecked, but it causes too much useless calls.

ANSWER (Plunker)

Angular Solutions


Solution 1 - Angular

Use a setter for the ViewChild:

 private contentPlaceholder: ElementRef;

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

The setter is called with an element reference once *ngIf becomes true.

Note, for Angular 8 you have to make sure to set { static: false }, which is a default setting in other Angular versions:

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

Note: if contentPlaceholder is a component you can change ElementRef to your component Class:

  private contentPlaceholder: MyCustomComponent;

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

Solution 2 - Angular

An alternative to overcome this is running the change detector manually.

You first inject the ChangeDetectorRef:

constructor(private changeDetector : ChangeDetectorRef) {}

Then you call it after updating the variable that controls the *ngIf

show() {
        this.display = true;
        this.changeDetector.detectChanges();
    }

Solution 3 - Angular

Angular 8+

You should add { static: false } as a second option for @ViewChild. This causes the query results to be resolved after change detection runs, allowing your @ViewChild to be updated after the value changes.

Example:

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

    display = false;

    constructor(private changeDetectorRef: ChangeDetectorRef) {
    }

    show() {
        this.display = true;

        // Required to access this.contentPlaceholder below,
        // otherwise contentPlaceholder will be undefined
        this.changeDetectorRef.detectChanges();

        console.log(this.contentPlaceholder);
    }
}

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

Solution 4 - Angular

The answers above did not work for me because in my project, the ngIf is on an input element. I needed access to the nativeElement attribute in order to focus on the input when ngIf is true. There seems to be no nativeElement attribute on ViewContainerRef. Here is what I did (following @ViewChild documentation):

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

I used setTimeout before focusing because the ViewChild takes a sec to be assigned. Otherwise it would be undefined.

Solution 5 - Angular

As was mention by others, the fastest and quickest solution is to use [hidden] instead of *ngIf. Taking this approach the component will be created but not visible, therefore you have access to it. This might not be the most efficient way.

Solution 6 - Angular

This could work but I don't know if it's convenient for your case:

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

ngAfterViewInit() {
 this.viewContainerRefs.changes.subscribe(item => {
   if(this.viewContainerRefs.toArray().length) {
     // shown
   }
 })
}

Solution 7 - Angular

Another quick "trick" (easy solution) is just to use [hidden] tag instead of *ngIf, just important to know that in that case Angular build the object and paint it under class:hidden this is why the ViewChild work without a problem. So it's important to keep in mind that you should not use hidden on heavy or expensive items that can cause performance issue

  <div class="addTable" [hidden]="CONDITION">

Solution 8 - Angular

My goal was to avoid any hacky methods that assume something (e.g. setTimeout) and I ended up implementing the accepted solution with a bit of RxJS flavour on top:

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

My scenario: I wanted to fire an action on a @ViewChild element depending on the router queryParams. Due to a wrapping *ngIf being false until the HTTP request returns the data, the initialization of the @ViewChild element happens with a delay.

How does it work: combineLatest emits a value for the first time only when each of the provided Observables emit the first value since the moment combineLatest was subscribed to. My Subject tabSetInitialized emits a value when the @ViewChild element is being set. Therewith, I delay the execution of the code under subscribe until the *ngIf turns positive and the @ViewChild gets initialized.

Of course don't forget to unsubscribe on ngOnDestroy, I do it using the ngUnsubscribe Subject:

  ngOnDestroy() {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }

Solution 9 - Angular

A simplified version, I had a similar issue to this when using the Google Maps JS SDK.

My solution was to extract the div and ViewChild into it's own child component which when used in the parent component was able to be hid/displayed using an *ngIf.

Before

HomePageComponent Template

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

HomePageComponent Component

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

After

MapComponent Template

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

MapComponent Component

@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 Template

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

HomePageComponent Component

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

Solution 10 - Angular

It Work for me if i use ChangeDetectorRef in 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();
}

Solution 11 - Angular

In my case I needed to load a whole module only when the div existed in the template, meaning the outlet was inside an ngif. This way everytime angular detected the element #geolocalisationOutlet it created the component inside of it. The module only loads once as well.

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>

Solution 12 - Angular

I think using defer from lodash makes a lot of sense especially in my case where my @ViewChild() was inside async pipe

Solution 13 - Angular

Working on Angular 8 No need to import ChangeDector

ngIf allows you not to load the element and avoid adding more stress to your application. Here's how I got it running without ChangeDetector

elem: ElementRef;

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

Then when I change my ngIf value to be truthy I would use setTimeout like this for it to wait only for the next change cycle:

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

This also allowed me to avoid using any additional libraries or imports.

Solution 14 - Angular

for Angular 8 - a mixture of null checking and @ViewChild static: false hackery

for a paging control waiting for async data

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

Solution 15 - Angular

Just make sur that the static option is set to false

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

Solution 16 - Angular

I had the same problem myself, with Angular 10.

If I tried to use [hidden] or *ngIf, then the @ViewChild variable was always undefined.

<p-calendar #calendar *ngIf="bShowCalendar" >
</p-calendar>

I fixed it by not removing it from the webpage.
I used an [ngClass] to make the control have opacity:0, and move it completely out of the way.

<style>
  .notVisible {
    opacity: 0;
    left: -1000px;
    position: absolute !important;
  }
</style>

<p-calendar #calendar [ngClass]="{'notVisible': bShowCalendar }" >
</p-calendar>

Yeah, I know, it's dumb and ugly, but it fixed the problem.

I also had to make the control static. I don't understand why.. but, again, it refused to work without this change:

export class DatePickerCellRenderer {
    @ViewChild('calendar', {static: true }) calendar: Calendar;

Solution 17 - Angular

We had a situation to set tabindex on ngif

html:

<div #countryConditional1 *ngIf="country=='USA'">        				 
<input id="streetNumber"  [(ngModel)]="streetNumber" pInputText>
</div>
			 
		

ts:

@ViewChild('countryConditional1') set countryConditional1(element){
		if (element){
			const container2 = document.querySelector("#someElement");
			container2.querySelector("span > input").setAttribute("tabindex", "18");}

Solution 18 - Angular

Read and Try this

Make sure passing the param { static: false } to @ViewChild resolve the problem.

**template.html code**

  <div *ngIf="showFirtChild">
    <first-child #firstchildComponent ></first-child>
  </div>

**in .ts file**

export class Parent implements 
{
  private firstChild: FirstchildComponent;

  @ViewChild('firstchildComponent', { static: false }) set content(content: 
  FirstchildComponent) {
     if(content) { 
          this.firstchildComponent = content;
     }
  }

 constructor(){}

  ShowChild(){
     this.showFirtChild = true;
     if(this.firstchildComponent){
        this.firstchildComponent.YourMethod()
     }
  }

}

Solution 19 - Angular

If setter doesn't seem to be working (not being called at all) with @ViewChild try @ContentChild instead.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionsinedsemView Question on Stackoverflow
Solution 1 - AngularparliamentView Answer on Stackoverflow
Solution 2 - AngularJefferson LimaView Answer on Stackoverflow
Solution 3 - AngularSviatoslav OleksivView Answer on Stackoverflow
Solution 4 - AngularzebracoView Answer on Stackoverflow
Solution 5 - Angularuser3728728View Answer on Stackoverflow
Solution 6 - AngularGünter ZöchbauerView Answer on Stackoverflow
Solution 7 - AngularOr YaacovView Answer on Stackoverflow
Solution 8 - AngularFilip JuncuView Answer on Stackoverflow
Solution 9 - AngularEugeneView Answer on Stackoverflow
Solution 10 - AngularIvan SimView Answer on Stackoverflow
Solution 11 - AngularGabb1995View Answer on Stackoverflow
Solution 12 - AngularTimoView Answer on Stackoverflow
Solution 13 - AngularManuel BMView Answer on Stackoverflow
Solution 14 - Angularjenson-button-eventView Answer on Stackoverflow
Solution 15 - AngularSmaillnsView Answer on Stackoverflow
Solution 16 - AngularMike GledhillView Answer on Stackoverflow
Solution 17 - AngularFeng ZhangView Answer on Stackoverflow
Solution 18 - Angularsaikumar yerra - skyView Answer on Stackoverflow
Solution 19 - AngularpopView Answer on Stackoverflow