Search code examples
javascriptangularangular-changedetection

Angular click event handler not triggering change detection


To put my problem simply, I have an element in component's template. This element has an ngIf condition and a (click) handler. It is not rendered from the very beginning, because the ngIf condition evaluates to false.

Now comes the interesting part: A code running outside the angular zone changes that condition to true, and after executing detectChanges on the change detector ref manually, this element gets rendered and the click handler ofc becomes active.

It all seems ok so far, but the problem is that when the (click) callback is run upon user's click, change detection is not triggered for the component.

Here is the reproduction https://stackblitz.com/edit/angular-kea4wi

Steps to reproduce it there:

  1. Click at the beige area
  2. Button appears, click it too
  3. Nothing happens, although message should have appeared below

Description:

  1. The beige area has a click event handler registered via addEventListener, and this event listener's callback is running outside the angular zone. Inside it a component's showButton property is set from false to true and I trigger change detection there manually by calling detectChanges(), otherwise the change in the showButton property wouldn't be registered. The code looks like this:

    this.zone.runOutsideAngular(() => {
       const el = this.eventTarget.nativeElement as HTMLElement;
       el.addEventListener('click', e => {
         this.showButton = true;
         this.cd.detectChanges();
       })
     })
    
  2. Now button appears, which thanks to *ngIf="showButton" wasn't rendered initially, and it has a click even handler declared in the template. This handler again changes component's property, this time showMessage to true.

    <button *ngIf="showButton" (click)="onButtonClick()">Click me!</button>
    
    onButtonClick() {
      this.showMessage = true;
    }
    
  3. When I click it, the handler obviously runs and changes component's showMessage to true, but it doesn't trigger change detection and message below doesn't appear. To make the example work, just set showButton to true from the very beginning, and the scenario above works.

The question is: How is this possible? Since I declared the (click) event handler in the template, shouldn't it always trigger change detection when called?


Solution

  • I created an issue in Angular's repo, and as it turns out, this behavior is logical, although perhaps unexpected. To rephrase what was written there by Angular team:

    The code which causes the element with (click) handler to render is running outside the Angular zone as stated in the question. Now, although I execute detectChanges() manually there, it doesn't mean that the code magically runs in angular zone all of a sudden. It runs the change detection all right, but it "stays" in a different zone. And as a result, when the element is about to be rendered, the element's click callback is created in and bound to non-angular zone. This in turn means that when it is triggered by user clicking, it is still called, but doesn't trigger change detection.

    The solution is to wrap code, which runs outside the angular zone, but which needs to perform some changes in the component, in zone.run(() => {...}).

    So in my stackblitz reproduction, the code running outside the angular zone would look like this:

        this.zone.runOutsideAngular(() => {
          const el = this.eventTarget.nativeElement as HTMLElement;
          el.addEventListener('click', e => {
            this.zone.run(() => this.showButton = true);
          })      
        })
    

    This, unlike calling detectChanges(), makes the this.showButton = true run in the correct zone, so that also elements created as a result of running that code with their event handlers are bound to the angular zone. This way, the event handlers always trigger change detection when reacting to DOM events.

    This all boils down to a following takeaway: Declaring event handlers in a template doesn't automatically guarantee change detection in all scenarios.