ChatGPT解决这个技术问题 Extra ChatGPT

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)

did you try using [hidden] attribute instead of *ngIf? It worked for me for a similar situation.

s
ssuperczynski

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

note that this setter is called initially with undefined content, so check for null if doing something in the setter
Good answer, but contentPlaceholder is ElementRef not ViewContainerRef.
How do you call the setter?
@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.
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.
J
Jefferson Lima

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

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)
Super helpful! Thanks!
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.
Any ideas why I might be getting this error when trying to call detectChanges(): ERROR TypeError: Cannot read property 'detectChanges' of undefined
N
Neistow

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


Thank you Sviatoslav. Tried everything above but only your solution worked.
This also worked for me (as did the viewchildren trick). This one is more intuitive and easier for angular 8.
This should be the accepted answer for the latest version.
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.
Probably the best solution for Angular 8+, but this.changeDetectorRef.detectChanges(); is indeed required
z
zebraco

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.


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.
You can detectChanges in showAsset() and not have to use the timeout.
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?
N
Neoheurist

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.


you have to note that using "[hidden]" may not work if the element is not of "display: block". better use [style.display]="condition ? '' : 'none'"
G
Günter Zöchbauer

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

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.
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=preview
This is actually a good answer. It works and I'm using this now. Thanks!
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;
Might be the change related to the const parameter of ViewChild
O
Or Yaacov

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

If that hidden is inside in another if then need to change many things
F
Filip Juncu

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

thanks a lot I've had the same issue, with tabSet & ngIf, your method saved me a lot of time and headache. Cheers m8 ;)
Had same issue and u saved my day. Thank you :)
E
Eugene

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

I
Ivan Sim

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

G
Gabb1995

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>

T
Timo

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


M
Manuel BM

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.


This solution worked for me. Adding setTimeout as suggested here did the trick.
j
jenson-button-event

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

S
Smaillns

Just make sur that the static option is set to false

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

M
Mike Gledhill

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;

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

s
saikumar yerra - sky

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

}

p
pop

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


@ContentChild is used for something else entirely.