Why is @ViewChild still undefined even though the *ngIf is set to true

Viewed 1196

I came across the following issue which was fixed by the {static: false} property in @ViewChild. This stackoverflow Q/A helped with that How should I use the new static option for @ViewChild in Angular 8?.

I wanted to understand this scenario better and how static changes the outcome, as well as how change detection impacts this scenario. I've done some reading about change detection in the angular documentation and have found that extremely lacking.

I came up with a stackblitz that illustrates something that I don't understand. Stackblitz angular example

When clicking toggle button twice, I get the following on the command line:

> undefined undefined
> undefined undefined
> undefined ElementRef {nativeElement: div}
> undefined ElementRef {nativeElement: div}

However I expect:

> undefined undefined
> undefined ElementRef {nativeElement: div}
> ElementRef {nativeElement: div} ElementRef {nativeElement: div}
> ElementRef {nativeElement: div} ElementRef {nativeElement: div}

Here is the logic for the code -- (see full code in stackblitz)

@Component({
  selector: "my-app",
  templateUrl: "./app.component.html",
  styleUrls: ["./app.component.css"]
})
export class AppComponent {
  @ViewChild("contentPlaceholder", { static: true })
  trueViewChild: ElementRef;

  @ViewChild("contentPlaceholder", { static: false })
  falseViewChild: ElementRef;

  display = false;

  constructor() {}

  show() {
    console.log(this.trueViewChild, this.falseViewChild);
    this.display = true;
    console.log(this.trueViewChild, this.falseViewChild);
  } 
}

My questions are:

  1. Why does the second row value of this.falseViewChild show as undefined. Shouldn't change detection have run after setting this.display = false and therefore it should not be undefined?
  2. Why does this.trueViewChild stay undefined. I would expect it to find the element after the *ngIf becomes true?
1 Answers

Angular change detections works with the help of zone.js library. Updating ViewChild/Content queries happens during change detection cycle.

The zone.js library patches async APIs(addEventListener, setTimeout(), Promises...) and knows exactly which task is executed and when it finished.

For example, it can listen to click event and emit notification when this task has been completed(there is no pending tasks, meaning that zone becomes stable).

Angular subscribes to those notification in order to perform change detection across all components three starting from root component.

// your code 
(click)="someHandler()" 

someHandler() {              
 ....
}

// angular core
checkStable() {
  if (there is no any task being executed and there is no any async pending request) {
    PERFORM CHANGE DETECTION
  } 
}

The order in the code about is the following:

 click
  ||
  \/
someHandler()
  ||
  \/
checkStable()
  ||
  \/
PERFORM CHANGE DETECTION

So, let's answer your questions:

  1. Why does the second row value of this.falseViewChild show as undefined. Shouldn't change detection have run after setting this.display = false and therefore it should not be undefined?

There is no reactivity when you change display property

show() {
 console.log(this.trueViewChild, this.falseViewChild);
 this.display = true;  <--- Angular doesn't do here anything, it only listens to zone state changes
 console.log(this.trueViewChild, this.falseViewChild); // nothing should be updated here 
                                                       // because there wasn't any cd cycle yet
} 

That's why you're getting the following output on the first click:

> undefined undefined
> undefined undefined   <---- nothing has updated

 ......
 update happens here

It will be updated later but you won't see this unless you click again because you don't log these values later.

  1. Why does this.trueViewChild stay undefined. I would expect it to find the element after the *ngIf becomes true?

Because there is the rule for this from Angular documentation:

With static queries (static: true), the query resolves once the view has been created, but before change detection runs. The result, though, will never be updated to reflect changes to your view, such as changes to ngIf and ngFor blocks.

It means that if it is initially false(e.g., it's inside *ngIf or ng-template) then it will be always false

Related