Reputation: 11888
Here is the template of main.html
:
<button (click)="displayAndUseMyCustomComp()"></button>
<my-custom-comp *ngIf="isMyCustomCompDisplayed" #myCustomComp="myCustomComp"></my-custom-comp>
and main.component.ts
:
export class MainComponent {
constructor() {}
private isMyCustomCompDisplayed boolean = false
@ViewChild('myCustomComp') myCustomComp: MyCustomComp
displayAndUseMyCustomComp() {
this.isMyCustomCompDisplayed = true
console.log(this.myCustomComp) // prints undefined
setTimeout(() => {
console.log(this.myCustomComp) // prints {object}
})
}
}
What's happening is that my template isn't yet refreshed after I set isMyCustomCompDisplayed
to true
. However, if I use a setTimeout
, myCustomComp
gets updated and my issue goes away. It is midly hacky, and I was wondering what was the correct way of doing what I am trying to.
Upvotes: 10
Views: 18290
Reputation: 105517
displayAndUseMyCustomComp
Angular updates ViewChild
query list as part of change detection. When Angular was running initial change detection the isMyCustomCompDisplayed
was false
and so myCustomComp
was hidden. The myCustomComp
was set to undefined
.
After you make a click the function displayAndUseMyCustomComp
is executed and isMyCustomCompDisplayed
is set to true
. Angular requires a change detection cycle to update the myCustomComp
query list. However, you try to read the value immediately and so it's still undefined
. You need to wait for another change detection cycle for Angular to update the query list.
setTimeout
If you try to read the myCustomComp
inside the timeout, Angular has a chance to run change detection between the update to isMyCustomCompDisplayed
and the time you read myCustomComp
.
When Angular runs change detection for the MainComponent
it detects that isMyCustomCompDisplayed
is updated. So it goes and updates bindings for ngIf
. It in turn reacts to this change and creates and embedded view with the myCustomComp
and attaches it to the MainComponent
component:
@Input()
set ngIf(condition: any) {
if (condidition) {
this._viewContainer.createEmbeddedView(this._thenTemplateRef, this._context);
If you're looking for synchronous solution, it will be available inside all lifecycle hooks that are executed after the view children query list is updated during the next change detection cycle that follows execution of displayAndUseMyCustomComp
. At the moment these are ngAfterViewInit
and ngAfterViewChecked
. Since the former is called only once, we need to use ngAfterViewChecked
:
ngAfterViewChecked() {
if(this.isMyCustomCompDisplayed) {
console.log(this.myCustomComp) // prints {object}
}
}
displayAndUseMyCustomComp() {
this.isMyCustomCompDisplayed = true
}
Another synchronous solution suggested by @ThinkingMedia
is also good. You can use ViewChildren
instead of ViewChild
and subscribe to changes
(btw you don't template reference):
@ViewChildren(myCustomComp) as: QueryList<myCustomComp>;
ngAfterViewInit() {
this.myCustomComp.changes.subscribe(() => {
console.log(this.myCustomComp.first); // prints {object}
});
}
The callback will be triggered during next digest when Angular will be updating query list (slightly earlier than ngAfterViewChecked
).
If you're looking for asynchronous solution, use setTimeout
as you do it. The Promise.resolve(null).then(()=>{ console.log(this.myCustomComp) })
(microtask) solution won't work because it will be executed after the current stack but before the change detection.
For more information on change detection read
Everything you need to know about change detection in Angular.
Upvotes: 23