【问题标题】:Angular2 “expression has changed after it was checked” exceptionAngular2“检查后表达式已更改”异常
【发布时间】:2017-06-26 12:35:44
【问题描述】:

我的模板中有以下按钮:

<button type="button" class="mgmButton" (click)="onSave()" [disabled]="saveDisabled()">Save</button>

根据 saveDisabled 函数的结果禁用按钮。

saveDisabled(): boolean {
    this.validationMessage = '';
    for (var i = 0; i < this.tableData.length; i++) {
        let row = this.tableData[i];
        if (row.edit) {
            if (row.data.roleCode == null || row.data.roleCode == '' ||
                row.data.grantProgramCode == null || row.data.grantProgramCode == '') {
                this.validationMessage = 'Row ' + (i + 1) + ' has not filled in all required fields. ';
            }
        }
    }

    if(this.validationMessage == '') {
        return false;
    } else {
        return true;
    }

该函数的早期版本没有构建validationMessage,它只是返回true 或false。这工作没有任何错误。但是当我将validationMessage属性添加到方法/组件/模板时,我开始收到“检查后表达式已更改”异常。

根据其他帖子,看起来会发生这种情况,因为我正在更改 validationMessage 变量,而更改检测仍在发生。我不确定我是否完全理解发生了什么或消除错误的最佳方法。

更新:

我创建了一个自定义验证器,它几乎可以完美运行。

我的组件有一个名为 tableData 的数据数组。 tableData 中的每一行都是一个对象,在模板的 html 表中显示为一行。有时一行处于只读模式,有时数据处于编辑模式,因此行中的某些列是输入字段、选择下拉列表等。

自定义验证器应用于表单标签。它将 tableData 作为输入。我所有的验证逻辑都有效,如果验证器返回错误,我会在模板中显示它。 (我确实必须将 tableData 转换为 json 字符串,然后对其进行解析以使组件和验证器之间的切换正常工作。)

但时间似乎有问题。假设给定的行处于编辑模式,并且用户更改了选择菜单的值。此选择绑定到 tableData 行之一中的属性。表单中的验证被触发了,但是传入的数据有select的旧值,而不是新值。本质上,表单的验证发生在表格行上的数据绑定更新支持对象之前。

【问题讨论】:

    标签: angular


    【解决方案1】:

    我通过从角核心添加 ChangeDetectionStrategy 解决了。

    import {  Component, ChangeDetectionStrategy } from '@angular/core';
    @Component({
      changeDetection: ChangeDetectionStrategy.OnPush,
      selector: 'page1',
      templateUrl: 'page1.html',
    })
    

    【讨论】:

      【解决方案2】:

      Vilmantas 解释得很好。您正在更改绑定 (validationMessage) 而不触发新一轮的更改检测,因此会出现错误。它作为一个警告,validationMessage 的当前值可能不会反映在 UI 中,直到将来出现确实触发更改检测。

      要修复,只需在更新验证消息后手动触发更改检测:

      import { ChangeDetectorRef } from '@angular/core';
      
      export class Whatever {
          constructor(private cdr: ChangeDetectorRef) {}
      
          myMethod() {
              // do stuff here
              this.cdr.detectChanges(); // detect changes
          }
      }
      

      Angular 确实内置了对表单的强大支持,不过,我同意这会更好。您现在拥有的这段代码似乎对每个表行的整个 tableData 数组进行了迭代,并且它将在更改检测期间运行多次。至少,您可能希望将i 作为参数传递给它(它在ngFor 循环中可用)并为自己保存for 循环。

      【讨论】:

      • 我会尝试再次触发更改检测。我做过一次,它导致调用堆栈的最大大小超出错误,但我认为我的方法逻辑有错误。该函数确实必须查看所有数据以确定保存按钮是否被禁用。我还有其他适用于单行的按钮和方法,在这种情况下,我确实将索引从 ngFor 传递给函数。
      【解决方案3】:

      @Vilmantas 正确地描述了正在发生的事情。但他没有说的是,这种方法总体上是不正确的。它应该是相反的方式 - 禁用标志应该基于在更改检测甚至开始之前已经计算的有效性标志来计算,而不是同时。

      Angular 2 为这个任务提供了特殊的验证机制:你可以在你的自定义指令中实现接口 Validator,并且这个相同的指令可以提供自己作为 NG_VALIDATORS 令牌的多提供者。以下是我的一个项目中的示例。

      此外,验证器可以是异步的(NG_ASYNC_VALIDATORS 令牌的提供者),这也是内置选项。在这种情况下,您可以考虑使用“pending”标志来绘制一些待验证的指示符,例如通过调用一些 Web 服务来检查用户名的可用性。

      在向 ngModel 提供此验证器后,您可以使用标准 ngModel.errors 映射与值,或 ngModel.valid 标志(以及其他标志 - 有一堆)来确定是否有任何错误并启用/禁用基于它的控件。使用这种方法,您甚至不需要考虑何时执行 - Angular 会为您处理一切。

      当然,你必须组织这个东西,以便它在模板上下文中可用,我的意思是在某些模块/组件等中正确导入/导出。

      import {Directive, forwardRef} from '@angular/core';
      import {Validator, AbstractControl, NG_VALIDATORS} from '@angular/forms';
      import {IDateValidationResult} from './date-validator.interfaces';
      import {moment} from '../../../shared/moment';
      
      const DATE_VALIDATOR = {
          provide: NG_VALIDATORS,
          useExisting: forwardRef(() => DateValidatorDirective),
          multi: true
      };
      
      @Directive({
          selector:
              '[whatever-selector-you-want]',
          providers: [DATE_VALIDATOR]
      })
      export class DateValidatorDirective implements Validator {
      
          validate = (control: AbstractControl): IDateValidationResult => {
              if (!control) {
                  return {required: true};
              }
              if (control.value === undefined) {
                  return {required: true};
              }
              if (control.value === null) {
                  return {invalidFormat: true};
              }
              let m = moment(control.value);
              if (!m.isValid()) {
                  return {invalidFormat: true};
              }
              return null;
          }
      
      }
      

      模板:

      <input type="date" [(ngModel)]="myModel" whatever-selector-you-want #myDateCtrl="ngModel">
      <div *ngIf="myDateCtrl.invalid">
          <div *ngIf="myDateCtrl.errors.required">The date is required</div>
          <div *ngIf="myDateCtrl.errors.invalidFormat">Date format is invalid</div>
      </div>
      

      【讨论】:

      • 我为基本表单创建了一些自定义验证器。在这种情况下我没有使用它,因为验证逻辑最终将跨越多个输入字段,并且还必须查看表中的其他行。第 1、2、3 列必须是表中所有行中的唯一组合。类似的东西。
      • @josh_in_dc,我看不出有什么区别。您可以以完全相同的方式为整个表单实现这个复杂的验证器,并检查表单中的所有数据 - 具有列/行的表,其值以某种方式相互关联,无论您的表单包含什么 - 任何您喜欢的方式。关键是验证不应该以任何方式由变更检测驱动。它应该由专门为此目的提供的任何角度来驱动。使用这种方法,我不在乎何时以及如何触发它以及何时在那里发生更改检测。它只是工作。
      • 我喜欢这种方法,但我有点不确定如何实现它。我见过的每个验证示例都在输入级别,而不是表单。验证器接口看起来只适用于 AbstractControls,当我查看 ngForm 的 API 文档时,它看起来不像是扩展了 AbstractControl。因此,如果我编写一个处理所有表单级别验证的方法,我如何将其与 Angular 的验证过程联系起来?
      • 所以我创建了一个自定义验证器,它只将一些信息记录到控制台并将其应用于我的表单并且它可以工作。所以我肯定错过了 API 文档中关于 ngForm 的一些内容。我可以改进验证器以将组件的业务数据对象作为输入字段。并非表中的所有内容都在表单控件中,因此传递给 validate 方法的抽象控件没有执行验证所需的所有数据。所以我可以将我需要的所有信息输入验证器来进行验证。所以我认为我可以通过 angular2 方式完成此操作。谢谢!
      【解决方案4】:

      我对 angular2 的内部结构不太熟悉,但我想它是这样的:

      • validationMessage 出现在模板上的 Save 按钮之前。
      • 第一次渲染模板时,validationMessage 没有值。
      • 算法继续,现在saveDisabled() 被调用,这反过来又改变了validatonMessage 的值。
      • => 现在 Angular 抱怨已经渲染/检查的 validationMessage 的值在一个渲染/检查周期内发生了变化。

      要解决这个问题,最好将验证代码移出saveDisabled() 方法并将布尔结果存储到一个字段中,或者您甚至可以使用相同的validationMessage 来禁用按钮:

      <button type="button" ... [disabled]="validationMessage">Save</button>
      

      (空或空字符串将评估为假,非空消息 - 为真)

      尝试将代码放在修改tableData 的位置。

      但是,如果您的 tableData 是某种形式,那么带有验证器(标准或自定义)的标准 angular2 形式很可能会更好。

      【讨论】:

      • 它是一个表单,但模板中没有表单标签。它只是表格中的输入字段,具有可变数量的行,用户可以在任何给定时间编辑。所有动作都由(更改)或(单击)事件触发。因此,我可以从更改 tableData 内容的所有位置调用 saveDisabled 函数。然后像您展示的那样更改按钮上的禁用输入。
      猜你喜欢
      • 2017-06-08
      • 2018-06-17
      • 2016-05-03
      • 2016-04-30
      • 2017-01-24
      • 1970-01-01
      • 2017-07-28
      • 2017-12-01
      • 1970-01-01
      相关资源
      最近更新 更多