Scipion
Scipion

Reputation: 11888

Angular4 How to know when a ViewChild has been reset

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

Answers (1)

Max Koretskyi
Max Koretskyi

Reputation: 105517

Here is why it's undefined after you call 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.

Here is why it's working if you wrap the call into 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.

Here is what happens during that change detection cycle

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);

When is the updated query list available

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

Related Questions