sinedsem
sinedsem

Reputation: 5723

@ViewChild in *ngIf

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)

Upvotes: 422

Views: 213737

Answers (19)

user3728728
user3728728

Reputation: 935

As was mention by others, the fastest and quickest solution is to use [style.display]="condition ? '' : 'none'" or [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.

Upvotes: 21

saikumar yerra - sky
saikumar yerra - sky

Reputation: 137

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

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

Upvotes: 3

Feng Zhang
Feng Zhang

Reputation: 1948

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

Upvotes: 0

pop
pop

Reputation: 3714

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

Upvotes: -1

Sviatoslav Oleksiv
Sviatoslav Oleksiv

Reputation: 2668

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

Upvotes: 131

Mike Gledhill
Mike Gledhill

Reputation: 29161

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;

Upvotes: 0

parliament
parliament

Reputation: 22914

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

Upvotes: 598

Smaillns
Smaillns

Reputation: 3127

Just make sur that the static option is set to false

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

Upvotes: 1

Ivan Sim
Ivan Sim

Reputation: 375

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

Upvotes: 3

jenson-button-event
jenson-button-event

Reputation: 18941

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

Upvotes: 1

Manuel BM
Manuel BM

Reputation: 888

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.

Upvotes: 1

Timo
Timo

Reputation: 81

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

Upvotes: 1

Gabb1995
Gabb1995

Reputation: 1037

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>

Upvotes: 1

Or Yaacov
Or Yaacov

Reputation: 3880

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

Upvotes: 12

Eugene
Eugene

Reputation: 211

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

My solution was to extract the divand 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;
 }

Upvotes: 3

Filip Juncu
Filip Juncu

Reputation: 362

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

Upvotes: 8

Jefferson Lima
Jefferson Lima

Reputation: 5396

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

Upvotes: 165

zebraco
zebraco

Reputation: 293

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.

Upvotes: 23

G&#252;nter Z&#246;chbauer
G&#252;nter Z&#246;chbauer

Reputation: 657338

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

Upvotes: 14

Related Questions