简体   繁体   English

Angular:尝试禁用按钮时出现 ExpressionChangedAfterItHasBeenCheckedError

[英]Angular: ExpressionChangedAfterItHasBeenCheckedError when trying to disable button

I use mat-dialog to edit details of my profile page.我使用 mat-dialog 来编辑个人资料页面的详细信息。 I'm getting an ExpressionChangedAfterItHasBeenCheckedError when I click the 'Edit age' button and the dialog window pops up.当我单击“编辑年龄”按钮并弹出对话框 window 时,我收到ExpressionChangedAfterItHasBeenCheckedError


I decided to extract the styling of all edit dialogs into a single edit.component:我决定将所有编辑对话框的样式提取到单个 edit.component 中:

edit.component.html编辑.component.html

<div class="navigation-control">
  <mat-icon (click)="onCancelButtonClicked()"
            class="close-button">close</mat-icon>
</div>

<div class="content-main">
  <ng-content select=".content-main"></ng-content>
</div>

<div class="content-bot">
  <button mat-raised-button
          (click)="onCancelButtonClicked()">Cancel</button>

  <button mat-raised-button
          (click)="onActionButtonClicked()"
          [lnDisableButton]="actionButtonDisabled">{{actionButtonValue}}</button>
</div>

edit.component.ts编辑.component.ts

@Component({ selector: 'ln-edit', ... })
export class EditComponent {
  @Input() actionButtonValue: string;
  @Input() actionButtonDisabled: boolean;
  @Output() cancelButtonClicked = new EventEmitter<void>();
  @Output() actionButtonClicked = new EventEmitter<void>();

  onCancelButtonClicked() {
    this.cancelButtonClicked.emit();
  }

  onActionButtonClicked() {
    this.actionButtonClicked.emit();
  }
}


To avoid the ExpressionChangedAfterItHasBeenCheckedError when trying to disable buttons and controls, I used this snippet.为避免在尝试禁用按钮和控件时出现 ExpressionChangedAfterItHasBeenCheckedError,我使用了此代码段。 But that didn't solve this issue.但这并没有解决这个问题。

disable-button.directive.ts禁用按钮.directive.ts

@Directive({ selector: '[lnDisableButton]' })
export class DisableButtonDirective {
  @Input('lnDisableButton') isDisabled = false;

  @HostBinding('attr.disabled')
  get disabled() { return this.isDisabled; }
}


The following is the contents of a mat-dialog window. This gets instantiated when I click the 'Edit age' button.以下是 mat-dialog window 的内容。当我单击“编辑年龄”按钮时,它会被实例化。 When I remove the [actionButtonDisabled]="actionButtonDisabled", the error goes away, but obivously I need that line to make the functionality disable the button.当我删除 [actionButtonDisabled]="actionButtonDisabled" 时,错误消失了,但显然我需要该行来使功能禁用按钮。

age-edit.component.html age-edit.component.html

<ln-edit [actionButtonValue]="actionButtonValue"
         [actionButtonDisabled]="actionButtonDisabled"
         (cancelButtonClicked)="onCancelButtonClicked()"
         (actionButtonClicked)="onActionButtonClicked()">
  <form [formGroup]="ageForm"
        class="content-main">
    <ln-datepicker formControlName="birthday"
                   [appearance]="'standard'"
                   [label]="'Birthday'"
                   class="form-field">
    </ln-datepicker>
  </form>
</ln-edit>


I handle the disabling/enabling the button in the 'ts' part of the mat-dialog popup.我在 mat-dialog 弹出窗口的“ts”部分处理禁用/启用按钮。

age-edit.component.ts年龄编辑.component.ts

@Component({ selector: 'ln-age-edit', ... })
export class AgeEditComponent implements OnInit, OnDestroy {
  ageForm: FormGroup;
  private initialFormValue: any;
  actionButtonDisabled = true;
  private unsubscribe = new Subject<void>();

  constructor(
    private editPhotoDialogRef: MatDialogRef<AgeEditComponent>,
    private fb: FormBuilder,
    @Inject(MAT_DIALOG_DATA) public dialogData: Date) { }

  ngOnInit() {
    this.initializeAgeForm();
    this.loadDataToAgeForm(this.dialogData);
    this.trackFormDistinct();
  }

  private initializeAgeForm(): void {
    this.ageForm = this.fb.group({
      birthday: null,
    });
  }

  loadDataToAgeForm(birthday: Date | null): void {
    if (!birthday) { return; }

    this.ageForm.setValue({ birthday });
    this.initialFormValue = this.ageForm.value;
  }

  get birthdayAC() { return this.ageForm.get('birthday') as AbstractControl; }

  get actionButtonValue(): string {
    return this.birthdayAC.value ? 'Update age' : 'Add age';
  }

  onCancelButtonClicked(): void {
    this.editPhotoDialogRef.close();
  }

  onActionButtonClicked(): void {
    this.editPhotoDialogRef.close({ ... });
  }

  trackFormDistinct(): void {
    this.ageForm.valueChanges.pipe(
      distinctUntilChanged(), // TODO: needed?
      takeUntil(this.unsubscribe)
    ).subscribe(val => {

      (this.formValueNotDistinct(this.ageForm.value, this.initialFormValue)
        || this.birthdayAC.value === null)
        ? this.actionButtonDisabled = true
        : this.actionButtonDisabled = false;
    });
  }

  ngOnDestroy() { ... }
}


I suspect this has something to do with content projection, but I'm not sure.我怀疑这与内容投影有关,但我不确定。 (...or perhaps with my custom 'ln-datepicker'?) (...或者可能是我的自定义“ln-datepicker”?)
Any ideas?有任何想法吗?
Thanks.谢谢。

From what I can tell, the problem resides in trackFormDistinct() method:据我所知,问题出在trackFormDistinct()方法中:

trackFormDistinct(): void {
  this.ageForm.valueChanges.pipe(
    distinctUntilChanged(), // TODO: needed?
    takeUntil(this.unsubscribe)
  ).subscribe(val => {

    (this.formValueNotDistinct(this.ageForm.value, this.initialFormValue)
      || this.birthdayAC.value === null)
      ? this.actionButtonDisabled = true
      : this.actionButtonDisabled = false;
  });
}

Looks like because of this.ageForm.valueChanges , will have different values in the 2 change detection cycles.看起来因为this.ageForm.valueChanges ,在 2 个变化检测周期中会有不同的值。 I think this.ageForm.valueChanges emits due to <ln-datepicker> .我认为this.ageForm.valueChanges由于<ln-datepicker>发出的。

In a tree of form controls , if one node calls setValue , all its ancestors will have to be updated.表单控件树中,如果一个节点调用setValue ,则必须更新其所有祖先。 I've written more about how Angular Forms work in this article .我在本文中详细介绍了 Angular Forms 的工作原理。

I'm thinking of 2 alternatives:我正在考虑两种选择:

  • skip the first emission of ageForm since it indicates the initialization of the form control tree, so this is irrelevant to the logic inside subscribe 's callback.跳过ageForm的第一次发射,因为它指示表单控件树的初始化,所以这与subscribe的回调中的逻辑无关。
this.ageForm.valueChanges.pipe(
  skip(1),
  distinctUntilChanged(), // TODO: needed?
  takeUntil(this.unsubscribe)
).subscribe(/* .... */)
  • initialize actionButtonDisabled with false , since the error complains that it switched from true to false使用false初始化actionButtonDisabled ,因为错误抱怨它从true切换到false
actionButtonDisabled = false;

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

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