简体   繁体   English

Angular 单击事件处理程序未触发更改检测

[英]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.这个元素有一个ngIf条件和一个(click)处理程序。 It is not rendered from the very beginning, because the ngIf condition evaluates to false .它不是从一开始就呈现的,因为 ngIf 条件的计算结果为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.现在是有趣的部分:在 angular 区域之外运行的代码将该条件更改为true ,并且在手动对更改检测器 ref 执行detectChanges之后,该元素被渲染并且点击处理程序 ofc 变为活动状态。

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.到目前为止一切似乎都还不错,但问题是当用户单击时运行(click)回调时,不会触发组件的更改检测

Here is the reproduction https://stackblitz.com/edit/angular-kea4wi这是复制品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 .米色区域有一个通过addEventListener注册的点击事件处理器,这个事件监听器的回调运行在angular区域之外 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.在其中,组件的showButton属性从false设置为true ,我通过调用detectChanges()在那里手动触发更改检测,否则不会注册showButton属性中的更改。 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.现在按钮出现了,这要归功于*ngIf="showButton"最初没有呈现,并且它有一个在模板中声明的点击事件处理程序。 This handler again changes component's property, this time showMessage to true .此处理程序再次更改组件的属性,这次将showMessagetrue

     <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.当我单击它时,处理程序显然会运行并将组件的showMessage更改为true ,但它不会触发更改检测并且不会出现下面的消息。 To make the example work, just set showButton to true from the very beginning, and the scenario above works.要使示例正常运行,只需从一开始就将 showButton 设置为 true,上面的场景就可以正常运行。

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?由于我在模板中声明了(click)事件处理程序,它不应该在调用时总是触发更改检测吗?

I created an issue in Angular's repo, and as it turns out, this behavior is logical , although perhaps unexpected.我在 Angular 的 repo 中创建了一个问题,事实证明,这种行为是合乎逻辑的,尽管可能出乎意料。 To rephrase what was written there by Angular team:改写 Angular 团队在那里写的内容:

The code which causes the element with (click) handler to render is running outside the Angular zone as stated in the question.如问题中所述,导致带有(click)处理程序的元素呈现的代码在 Angular 区域之外运行。 Now, although I execute detectChanges() manually there, it doesn't mean that the code magically runs in angular zone all of a sudden.现在,虽然我在那里手动执行detectChanges() ,但这并不意味着代码突然神奇地在angular 区域运行。 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 .结果,当元素即将被渲染时,元素的点击回调被创建并绑定到 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(() => {...}) .解决方案是包装代码,该代码在 angular 区域之外运行,但需要在组件中执行一些更改,在zone.run(() => {...})中。

So in my stackblitz reproduction, the code running outside the angular zone would look like this:所以在我的 stackblitz 复制中,在 angular 区域之外运行的代码如下所示:

    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.与调用detectChanges()不同,这使得this.showButton = true在正确的区域中运行,因此由于运行该代码及其事件处理程序而创建的元素也被绑定到 angular 区域。 This way, the event handlers always trigger change detection when reacting to DOM events.这样,事件处理程序在响应 DOM 事件时总是触发更改检测

This all boils down to a following takeaway: Declaring event handlers in a template doesn't automatically guarantee change detection in all scenarios .这一切都归结为以下要点:在模板中声明事件处理程序并不能自动保证在所有场景中都能检测到更改

In case someone wants to do tasks that don't trigger change detection, here is how:如果有人想执行不触发更改检测的任务,方法如下:

import { NgZone }from '@angular/core';

taskSelection;

constructor
// paramenters
(
  private _ngZone: NgZone,
)
// code block
{}


  /*
      Angular Lifecycle hooks
  */
ngOnInit() {
    this.processOutsideOfAngularZone();
}

processOutsideOfAngularZone () {
    var _this = this;

    this._ngZone.runOutsideAngular(() => {
    
      document.onselectionchange = function() {
          console.log('Outside ngZone Done!'); 

          let selection = document.getSelection();
          _this.taskSelection["anchorNode"] = selection.anchorNode.parentElement;
          _this.taskSelection["anchorOffset"] = selection.anchorOffset;
          _this.taskSelection["focusOffset"] = selection.focusOffset;
          _this.taskSelection["focusNode"] = selection.focusNode;
          _this.taskSelection["rangeObj"] = selection.getRangeAt(0);
      }

    });
}

https://angular.io/api/core/NgZone#runOutsideAngular https://angular.io/api/core/NgZone#runOutsideAngular

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM