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 div
and 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