393

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)

ng-hobby
  • 2,077
  • 2
  • 13
  • 26
sinedsem
  • 5,413
  • 7
  • 29
  • 46
  • 5
    did you try using [hidden] attribute instead of *ngIf? It worked for me for a similar situation. – Shardul Jan 27 '17 at 21:39

19 Answers19

564

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;
     }
  }
ssuperczynski
  • 3,190
  • 3
  • 44
  • 61
parliament
  • 21,544
  • 38
  • 148
  • 238
  • 34
    note that this setter is called initially with undefined content, so check for null if doing something in the setter – Recep Jun 30 '17 at 09:10
  • 1
    Good answer, but `contentPlaceholder` is `ElementRef` not `ViewContainerRef`. – developer033 Sep 09 '17 at 17:49
  • 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 in the first place, but it probably would have blown up with AOT compilation anyways. – parliament Dec 01 '17 at 18:39
  • 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. – Chris K. Sep 26 '18 at 03:23
  • 7
    How do you call the setter? – Leandro Cusack Nov 06 '18 at 19:12
  • 3
    @LeandroCusack it gets called automatically when Angular finds ```
    ```. 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.
    – parliament Nov 14 '18 at 15:05
  • Just don't forget to detect changes, in case you're using that child as an input to another child component. Example: https://stackblitz.com/edit/ngif-var-template-error – Ezra Steinmetz Nov 27 '18 at 18:37
  • 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).` – Yona Appletree Jan 03 '19 at 22:38
  • 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 } } ` – Gabb1995 Apr 26 '19 at 06:41
  • This works, but where is this in the official documentation? – DSLuminary Jul 09 '19 at 17:53
  • 3
    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'" – zhuhang.jasper Oct 21 '19 at 02:56
  • 1
    @zhuhang.jasper the setter name is not important. I had two instances of @ViewChild, and simply named the setters `content1` and `content2` with success. – kebab-case Oct 25 '19 at 14:51
  • 4
    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. – nospamthanks Jan 27 '20 at 15:23
  • 2
    I also had to add {read: ElementRef}, in addition to static: false to my @ViewChild for this to work for a custom component. See https://stackoverflow.com/questions/45921819/angular-nativeelement-is-undefined-on-viewchild – crowmagnumb Apr 13 '20 at 18:36
  • 1
    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 ;) – ktsangop Mar 12 '21 at 10:23
  • 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. – Hrvoje Batrnek Jan 06 '22 at 19:43
  • Good trick! Will make use of it in some scenarios. Thank you. – Alexander Sep 20 '22 at 15:01
  • Solved my problem after working on it for 2-3 hours – Marian07 Dec 29 '22 at 14:33
158

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();
    }
Jefferson Lima
  • 5,186
  • 2
  • 28
  • 28
  • 3
    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) – kebab-case Oct 25 '19 at 14:52
  • Super helpful! Thanks! – AppDreamer Mar 17 '20 at 00:53
  • 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. – andreas Oct 02 '20 at 20:25
  • Any ideas why I might be getting this error when trying to call detectChanges(): ERROR TypeError: Cannot read property 'detectChanges' of undefined – J.D. Oct 05 '20 at 15:06
120

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

Neistow
  • 806
  • 1
  • 8
  • 20
Sviatoslav Oleksiv
  • 2,538
  • 2
  • 13
  • 12
  • 3
    Thank you Sviatoslav. Tried everything above but only your solution worked. – Peter Drinnan Aug 22 '19 at 01:23
  • This also worked for me (as did the viewchildren trick). This one is more intuitive and easier for angular 8. – Alex Aug 28 '19 at 21:06
  • 2
    This should be the accepted answer for the latest version. – Krishna Prashatt Sep 30 '19 at 11:51
  • i must wait few ms after detectchanges with setTimeout – david valentino Dec 21 '19 at 08:45
  • 2
    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. – Ruan Mendes Jan 17 '20 at 11:33
  • 3
    Probably the best solution for Angular 8+, but `this.changeDetectorRef.detectChanges();` is indeed required – mirushaki Mar 29 '20 at 11:19
  • 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. – Loic Jun 02 '20 at 01:17
  • 2
    "Starting with version 9, the static flag will default to false." from https://angular.io/guide/static-query-migration – Borneo777 Dec 16 '20 at 11:02
  • Clean and well understood, arigato sensei... – bloo Jan 20 '22 at 07:56
22

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.

zebraco
  • 283
  • 3
  • 9
  • 2
    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. – Will Shaver Sep 06 '17 at 21:41
  • You can detectChanges in showAsset() and not have to use the timeout. – WrksOnMyMachine Jun 12 '19 at 21:36
  • 1
    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?` – Ruan Mendes Jan 17 '20 at 11:35
17

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.

Pavel Chuchuva
  • 22,633
  • 10
  • 99
  • 115
user3728728
  • 885
  • 2
  • 11
  • 14
  • 2
    you have to note that using "[hidden]" may not work if the element is not of "display: block". better use [style.display]="condition ? '' : 'none'" – Félix Brunet May 08 '19 at 19:29
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
   }
 })
}
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 1
    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. – Günter Zöchbauer Sep 07 '16 at 10:37
  • Sorry, I was wrong. `AfterViewInit` actually works. I've removed all my comments in order not to confuse people. Here is a working Plunker: http://plnkr.co/edit/myu7qXonmpA2hxxU3SLB?p=preview – sinedsem Sep 07 '16 at 11:30
  • 1
    This is actually a good answer. It works and I'm using this now. Thanks! – Konstantin Jul 11 '19 at 16:14
  • 1
    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; – Alex Aug 28 '19 at 21:01
  • Might be the change related to the `const` parameter of `ViewChild` – Günter Zöchbauer Aug 29 '19 at 03:14
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">
Or Yaacov
  • 3,597
  • 5
  • 25
  • 49
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();
  }
Filip Juncu
  • 352
  • 2
  • 10
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;
 }
Eugene
  • 211
  • 2
  • 9
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();
}
Ivan Sim
  • 305
  • 4
  • 15
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>
Gabb1995
  • 977
  • 10
  • 18
1

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

Timo
  • 81
  • 1
  • 5
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.

Manuel BM
  • 868
  • 1
  • 13
  • 17
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);
  });
}
jenson-button-event
  • 18,101
  • 11
  • 89
  • 155
1

Just make sur that the static option is set to false

  @ViewChild('contentPlaceholder', {static: false}) contentPlaceholder: ElementRef;
Smaillns
  • 2,540
  • 1
  • 28
  • 40
1

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()
     }
  }
}
ssuperczynski
  • 3,190
  • 3
  • 44
  • 61
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;
Mike Gledhill
  • 27,846
  • 7
  • 149
  • 159
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");
  }
ssuperczynski
  • 3,190
  • 3
  • 44
  • 61
Feng Zhang
  • 1,698
  • 1
  • 17
  • 20
-1

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

pop
  • 3,464
  • 3
  • 26
  • 43