Angular2“表达式在检查后发生了变化”异常

时间:2017-02-08 18:23:47

标签: angular

我的模板中有以下按钮:

<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的旧值,而不是新值。本质上,表单的验证发生在表行上的数据绑定更新后备对象之前。

4 个答案:

答案 0 :(得分:9)

我通过从angular core添加ChangeDetectionStrategy来解决。

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

答案 1 :(得分:1)

维尔曼塔斯解释得非常好。您正在更改绑定(validationMessage)而不会触发新一轮的更改检测,从而导致错误。它可以作为警告,validationMessage的当前值可能不会反映在用户界面中,直到将来出现 触发变更检测的事情。

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

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循环。

答案 2 :(得分:0)

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

  • validationMessage出现在模板上的Save按钮之前。
  • 首次呈现模板时,validationMessage没有值。
  • 算法继续,现在调用saveDisabled(),然后更改validatonMessage值。
  • =&GT; now angular抱怨已经渲染/已检查validationMessage的值在一个渲染/检查周期内发生了变化。

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

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

(空或空字符串将计算为false,非空消息 - 为true)

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

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

答案 3 :(得分:0)

@Vilmantas正确描述了正在发生的事情。他没有说的是这种方法一般是不正确的。它应该是相反的方式 - 禁用标志应该根据有效标志计算,该标志在变化检测开始之前已经计算,而不是同时开始计算。

Angular 2为此任务提供了特殊的验证机制:接口验证器,您可以在自定义指令中实现,并且此相同的指令可以将自身提供为NG_VALIDATORS令牌的多提供程序。以下是我的一个项目的例子。

此外,验证器可以是异步的(NG_ASYNC_VALIDATORS令牌的提供者),这也是内置选项。在这种情况下,您可以考虑“挂起”标志来绘制一些待处理验证的指示,例如通过调用某些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>