5

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?
critrange
  • 5,652
  • 2
  • 16
  • 47
Jac Frall
  • 403
  • 7
  • 15

1 Answers1

1

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

yurzui
  • 205,937
  • 32
  • 433
  • 399
  • Thanks for your in depth answer. Question #2 makes sense now. Your forked stackblitz shows nothing after the first click and only something after the second click `[object Object]`. I just want to check if that is the intended effect you had? – Jac Frall Aug 06 '20 at 13:16
  • Yes, you're right I looked at different behavior. It should update view on after the second click as well. – yurzui Aug 06 '20 at 13:22
  • It is still somewhat unclear for the first question. So is the `console.log(...)` after the `this.display = true;` undefined because Angular has not yet received the value from the subscription from zone.js or is it because zone.js doesn't look at those kind of changes? – Jac Frall Aug 06 '20 at 13:26
  • This code is executed inside Angular zone and zone knows that something is being executed only after this handler executed Angular checks state of the zone and only if there is no pending tasks then it emits onMicrotaskEmpty event. There is already subscription to this event in order to run change detection cycle – yurzui Aug 06 '20 at 13:29
  • Here's how Angular checks for state of zone https://github.com/angular/angular/blob/d7c043ba350def55b8247ab94106fadceb3cbfb3/packages/core/src/zone/ng_zone.ts#L253 – yurzui Aug 06 '20 at 13:30
  • Here's how Angular emits event https://github.com/angular/angular/blob/d7c043ba350def55b8247ab94106fadceb3cbfb3/packages/core/src/zone/ng_zone.ts#L256 – yurzui Aug 06 '20 at 13:30
  • Here's the subscription https://github.com/angular/angular/blob/d7c043ba350def55b8247ab94106fadceb3cbfb3/packages/core/src/application_ref.ts#L598-L601 where Angular calls `tick` method in order to execute change detection cycle – yurzui Aug 06 '20 at 13:31
  • I think I understand now. So Zone.js waits until the `someHandler()` execution ends before running change detection? If so, then that leaves one question, why didn't your stackblitz work like we thought it would – Jac Frall Aug 06 '20 at 13:40
  • Let's take a look again https://stackblitz.com/edit/angular-hcmtnu?file=src%2Fapp%2Fapp.component.html That's because we're getting `ExpressionChangedAfterItHasBeenCheckedError` Why? Because Angular first updates html then updates Queries. And you might be aware of two change detection runs. – yurzui Aug 06 '20 at 13:49
  • So, after `someHandler()` Angular goes to template and checks `falseViewChild`. It hasn't been updated yet because Queries update happen after template check. It remember latest value of `falseViewChild`. After that it updates Query and component gets updated `falseViewChild` value. Now it's time to second cd cycle. Angular goes to template and checks remembered value (`undefined`) with the current value `Object` and throws error that it has changed during change detection – yurzui Aug 06 '20 at 13:52
  • Two change detection cycles are executed here https://github.com/angular/angular/blob/d7c043ba350def55b8247ab94106fadceb3cbfb3/packages/core/src/application_ref.ts#L727-L731 – yurzui Aug 06 '20 at 13:54
  • Here's where we can notice the order for updateRenderer(where Angular updates template) and execQueriesActions(where Angular updates ViewChild/ren) https://github.com/angular/angular/blob/d7c043ba350def55b8247ab94106fadceb3cbfb3/packages/core/src/view/view.ts#L379-L382 – yurzui Aug 06 '20 at 13:57
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/219347/discussion-between-jac-frall-and-yurzui). – Jac Frall Aug 06 '20 at 14:46