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)
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.
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();
}
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.
As was mention by other, the fastest and quickest solution is to use [hidden] instead of *ngIf, this way the component will be created but not visible, there for you can have access to it, though it might not be the most efficient way.
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
}
})
}
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">
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();
}
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;
}
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>
I think using defer from lodash makes a lot of sense especially in my case where my @ViewChild() was inside async pipe
This was working for me. Just apply some delay after binding data to form.
setTimeout(() => { this.changeDetector.detectChanges(); }, 500);
来源:https://stackoverflow.com/questions/39366981/viewchild-in-ngif