@ViewChild in *ngIf
Asked Answered
O

19

412

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)

Ossy answered 7/9, 2016 at 10:8 Comment(1)
did you try using [hidden] attribute instead of *ngIf? It worked for me for a similar situation.Intracranial
S
583

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;
     }
  }
Showroom answered 12/12, 2016 at 7:4 Comment(18)
note that this setter is called initially with undefined content, so check for null if doing something in the setterGarwin
Good answer, but contentPlaceholder is ElementRef not ViewContainerRef.Incivility
I ran into a related issue recently, it was still passing undefined even after *ngIf was true. That turned out to be caused by not including the Component in my NgModule. For ex I did @ViewChild(MatCheckbox) when MatCheckbox was not registered in NgModule. I'm not sure why the template didn't throw an error when I used <mat-checkbox> in the first place, but it probably would have blown up with AOT compilation anyways.Showroom
Answer also works in conjunction with the fact that ElementRef will be undefined even inside the ngAfterViewInit() lifecycle event with directives such as *ngIf since element references after processed after such directives are done with evaluation.Deponent
How do you call the setter?Sumbawa
@LeandroCusack it gets called automatically when Angular finds <div #contentPlaceholder></div>. Technically you can call it manually like any other setter this.content = someElementRef but I don't see why you would want to do that.Showroom
Just don't forget to detect changes, in case you're using that child as an input to another child component. Example: stackblitz.com/edit/ngif-var-template-errorPollypollyanna
This doesn't work for me using the AOT compiler: Only initialized variables and constants can be referenced in decorators because the value of this variable is needed by the template compiler in 'ViewChild': 'ViewChild' is not initialized at ../@angular/core/src/metadata/di.ts(360,22).Abhorrence
For me it was still calling the function even though the ngif was false, so as a fix I had to do this: @ViewChild('geolocalisationOutlet', {read: ViewContainerRef}) set geolocalisation(geolocalisationOutlet: ViewContainerRef) { if (geolocalisationOutlet) { // do stuff here } } Slug
This works, but where is this in the official documentation?Softfinned
The setter name content is important or we can rename it? What if you have many elements like this? You'll see "Duplicate identifier 'content'"Gotama
@Gotama the setter name is not important. I had two instances of @ViewChild, and simply named the setters content1 and content2 with success.Newt
Just a helpful note for anyone who comes across this now - you need to have @ViewChild('myComponent', {static: false}) where the key bit is the static: false, which allows it to take different inputs.Brickey
I also had to add {read: ElementRef}, in addition to static: false to my @ViewChild for this to work for a custom component. See #45922319Gleeful
Good answer, but I was trying to view a child component's child, so that obviously didn't work. Just a reminder to double check, if this answer still does not work for you ;)Wonky
Whatever I do it will not get ViewContainerRef if ngIf is used. set is called but with undefined value, when I remove ngIf, set argument has value.Puryear
Good trick! Will make use of it in some scenarios. Thank you.Truong
Solved my problem after working on it for 2-3 hoursDependable
S
162

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();
    }
Spriggs answered 4/9, 2017 at 20:46 Comment(4)
Thanks! I was using the accepted answer but it was still causing an error because the children were still undefined when I tried to use them sometime after onInit(), so I added the detectChanges before calling any child function and it fixed it. (I used both the accepted answer and this answer)Newt
Super helpful! Thanks!Felixfeliza
I had to run the CDR as well, the ViewChild was not updated soon enough when I needed it. This may happen if you rely on the child in the same function as you update the *ngIf property. In that case, the changes may not have been detected yet and the ViewChild property may still be undefined.Pernick
Any ideas why I might be getting this error when trying to call detectChanges(): ERROR TypeError: Cannot read property 'detectChanges' of undefinedAshely
E
129

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

Endymion answered 15/8, 2019 at 10:45 Comment(10)
Thank you Sviatoslav. Tried everything above but only your solution worked.Lisk
This also worked for me (as did the viewchildren trick). This one is more intuitive and easier for angular 8.Decasyllable
This should be the accepted answer for the latest version.Laboured
I am using <mat-horizontal-stepper *ngIf="viewMode === DialogModeEnum['New']" linear #stepper, @ViewChild('stepper', {static: true}) private stepper: MatStepper; and this.changeDetector.detectChanges(); and it still doesn't work.Psycholinguistics
i must wait few ms after detectchanges with setTimeoutGrissel
The text of the answer is missing the fact that you have to call detectChanges which does not seem like something you should do, I would much rather have a setter and not have to inject extra cruft into my component. Not to mention the two comments above saying it doesn't work... so I don't agree that this should be the accepted answer, it's an alternative.Noncommittal
Probably the best solution for Angular 8+, but this.changeDetectorRef.detectChanges(); is indeed requiredImpaste
I wonder if there is some better option, because most time we use static:false for the dynamic element, but if my component is static after the ngIf flag is identified, i prefer to delay the value getter, but after it get the value, treat the element as a static one because it won't change again.Faber
"Starting with version 9, the static flag will default to false." from angular.io/guide/static-query-migrationTypehigh
Clean and well understood, arigato sensei...Alongshore
P
23

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.

Provincetown answered 25/4, 2017 at 22:53 Comment(3)
A setTimeout() of 0 worked for me. My element hidden by my ngIf was correctly bound after a setTimeout, without the need for the set assetInput() function in the middle.Antetype
You can detectChanges in showAsset() and not have to use the timeout.Propst
How's this an answer? The OP already mentioned using a setTimeout? I usually wrap all required actions into setTimeout(()=>{},1) as shown above. Is there a more correct way?Noncommittal
C
19

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.

Chavez answered 29/1, 2017 at 9:55 Comment(1)
you have to note that using "[hidden]" may not work if the element is not of "display: block". better use [style.display]="condition ? '' : 'none'"Daisy
U
14

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
   }
 })
}
Unbelief answered 7/9, 2016 at 10:21 Comment(5)
Can you please try ngAfterViewInit() instead of ngOnInit(). I assumed that viewContainerRefs is already initialized but doesn't yet contain items. Seems I remembered this wrong.Pestilent
Sorry, I was wrong. AfterViewInit actually works. I've removed all my comments in order not to confuse people. Here is a working Plunker: plnkr.co/edit/myu7qXonmpA2hxxU3SLB?p=previewOssy
This is actually a good answer. It works and I'm using this now. Thanks!Annabelleannabergite
This worked for me after upgrade from angular 7 to 8. For some reason, the upgrade caused the component to be undefined in afterViewInit even with using static: false per the new ViewChild syntax when the component was wrapped in an ngIf. Also note that the QueryList requires a type now like this QueryList<YourComponentType>;Decasyllable
Might be the change related to the const parameter of ViewChildPestilent
N
11

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">
Nope answered 12/1, 2019 at 17:19 Comment(1)
If that hidden is inside in another if then need to change many thingsMeggs
I
8

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();
  }
Ichnography answered 23/5, 2018 at 18:59 Comment(2)
thanks a lot I've had the same issue, with tabSet & ngIf, your method saved me a lot of time and headache. Cheers m8 ;)Afraid
Had same issue and u saved my day. Thank you :)Marigraph
H
3

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;
 }
Hummingbird answered 18/11, 2018 at 13:11 Comment(0)
C
3

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();
}
Caladium answered 12/5, 2020 at 4:36 Comment(0)
W
3

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()
     }
  }
}
Worlock answered 20/5, 2022 at 13:59 Comment(0)
S
1

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>
Slug answered 12/3, 2019 at 13:27 Comment(0)
S
1

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

Spoils answered 12/6, 2019 at 16:10 Comment(0)
Z
1

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.

Zoophyte answered 6/9, 2019 at 1:54 Comment(1)
This solution worked for me. Adding setTimeout as suggested here did the trick.Simmers
B
1

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);
  });
}
Beefwood answered 20/12, 2019 at 13:55 Comment(0)
B
1

Just make sur that the static option is set to false

  @ViewChild('contentPlaceholder', {static: false}) contentPlaceholder: ElementRef;
Benedic answered 21/7, 2020 at 11:25 Comment(0)
K
0

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;
Kwh answered 28/1, 2021 at 14:11 Comment(0)
S
0

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");
  }
Swathe answered 14/4, 2022 at 14:8 Comment(0)
R
-1

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

Reprove answered 9/8, 2021 at 22:18 Comment(1)
@ContentChild is used for something else entirely.Clarinda

© 2022 - 2024 — McMap. All rights reserved.