Reputation: 5723
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.
Upvotes: 422
Views: 213737
Reputation: 935
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.
Upvotes: 21
Reputation: 137
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()
}
}
}
Upvotes: 3
Reputation: 1948
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");
}
Upvotes: 0
Reputation: 3714
If setter doesn't seem to be working (not being called at all) with @ViewChild
try @ContentChild
instead.
Upvotes: -1
Reputation: 2668
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
Upvotes: 131
Reputation: 29161
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;
Upvotes: 0
Reputation: 22914
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;
}
}
Upvotes: 598
Reputation: 3127
Just make sur that the static option is set to false
@ViewChild('contentPlaceholder', {static: false}) contentPlaceholder: ElementRef;
Upvotes: 1
Reputation: 375
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();
}
Upvotes: 3
Reputation: 18941
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);
});
}
Upvotes: 1
Reputation: 888
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.
Upvotes: 1
Reputation: 81
I think using defer from lodash makes a lot of sense especially in my case where my @ViewChild()
was inside async
pipe
Upvotes: 1
Reputation: 1037
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>
Upvotes: 1
Reputation: 3880
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">
Upvotes: 12
Reputation: 211
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;
}
Upvotes: 3
Reputation: 362
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();
}
Upvotes: 8
Reputation: 5396
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();
}
Upvotes: 165
Reputation: 293
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.
Upvotes: 23
Reputation: 657338
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
}
})
}
Upvotes: 14