Angular 2 @ViewChild in *ngIf

2019-01-05 09:27发布

Question

What is the most elegant way to get @ViewChild after corresponding element in template was shown? Below is an example. Also Plunker available.

Template:

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

Component:

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)

8条回答
手持菜刀,她持情操
2楼-- · 2019-01-05 09:45

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;
 }
查看更多
该账号已被封号
3楼-- · 2019-01-05 09:48

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();
  }
查看更多
淡お忘
4楼-- · 2019-01-05 09:58

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();
    }
查看更多
走好不送
5楼-- · 2019-01-05 09:58

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.

查看更多
叛逆
6楼-- · 2019-01-05 10:00

The accepted answer using a QueryList did not work for me. However what did work was using a setter for the ViewChild:

 private contentPlaceholder: ElementRef;

 @ViewChild('contentPlaceholder') set content(content: ElementRef) {
    this.contentPlaceholder = content;
 }

The setter is called once *ngIf becomes true.

查看更多
祖国的老花朵
7楼-- · 2019-01-05 10:04

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
   }
 })
}
查看更多
登录 后发表回答