Angular自定义组件中的不一致验证问题 [英] Inconsistent validation issue in Angular custom component

查看:101
本文介绍了Angular自定义组件中的不一致验证问题的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

为了展示一种真实的示例,假设我们要在应用程序中使用@ angular/material的datepicker.

我们想在很多页面上使用它,因此我们希望可以很容易地将它添加到任何地方都具有相同配置的表单中.为满足此需求,我们使用ControlValueAccessor实现在<mat-datepicker>周围创建自定义角度分量,以便能够在其上使用[(ngModel)].

我们要处理组件中的典型验证,但同时,我们希望使验证结果可用于包含CustomDatepickerComponent的外部组件.

作为一个简单的解决方案,我们可以像这样实现validate()方法(innerNgModel来自导出的ngModel:#innerNgModel="ngModel".请参阅此问题结尾的完整代码):

validate() {
    return (this.innerNgModel && this.innerNgModel.errors) || null;
}

在这一点上,我们可以以一种非常简单的方式(根据需要)在任何表单组件中使用日期选择器:

<custom-datepicker [(ngModel)]="myDate"></custom-datepicker>

我们还可以扩展上面的行以具有更好的调试体验(像这样):

<custom-datepicker [(ngModel)]="myDate" #date="ngModel"></custom-datepicker>
<pre>{{ date.errrors | json }}</pre>

只要我更改自定义datepicker组件中的值,一切就可以正常工作.如果日期选择器有任何错误,则周围的表格将保持无效(如果日期选择器有效,则该表格将变为有效).

但是!

如果外部表单组件的myDate成员(作为ngModel传递的成员)被外部组件更改(如:this.myDate= null),则会发生以下情况:

  1. CustomDatepickerComponent的writeValue()运行,并更新日期选择器的值.
  2. CustomDatepickerComponent的validate()运行,但是此时innerNgModel尚未更新,因此它返回对较早状态的验证.

要解决此问题,我们可以在setTimeout中发出来自组件的更改:

public writeValue(data) {
    this.modelValue = data ? moment(data) : null;
    setTimeout(() => { this.emitChange(); }, 0);
}

在这种情况下,emitChange(自定义组件的广播更改)将触发新的验证.并且由于setTimeout,它将在下一个周期中已经更新innerNgModel时运行.


我的问题是,是否有比使用setTimeout更好的方法来解决此问题? 而且,如果可能的话,我会坚持使用模板驱动的实现.

提前谢谢!


示例的完整源代码:

custom-datepicker.component.ts

import {Component, forwardRef, Input, ViewChild} from '@angular/core';
import {ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgModel} from '@angular/forms';
import * as moment from 'moment';
import {MatDatepicker, MatDatepickerInput, MatFormField} from '@angular/material';
import {Moment} from 'moment';

const AC_VA: any = {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CustomDatepickerComponent),
    multi: true
};

const VALIDATORS: any = {
    provide: NG_VALIDATORS,
    useExisting: forwardRef(() => CustomDatepickerComponent),
    multi: true,
};

const noop = (_: any) => {};

@Component({
    selector: 'custom-datepicker',
    templateUrl: './custom-datepicker.compnent.html',
    providers: [AC_VA, VALIDATORS]
})
export class CustomDatepickerComponent implements ControlValueAccessor {

    constructor() {}

    @Input() required: boolean = false;
    @Input() disabled: boolean = false;
    @Input() min: Date = null;
    @Input() max: Date = null;
    @Input() label: string = null;
    @Input() placeholder: string = 'Pick a date';

    @ViewChild('innerNgModel') innerNgModel: NgModel;

    private propagateChange = noop;

    public modelChange(event) {
        this.emitChange();
    }

    public writeValue(data) {
        this.modelValue = data ? moment(data) : null;
        setTimeout(() => { this.emitChange(); }, 0);
    }

    public emitChange() {
        this.propagateChange(!this.modelValue ? null : this.modelValue.toDate());
    }

    public registerOnChange(fn: any) { this.propagateChange = fn; }

    public registerOnTouched() {}

    validate() {
        return (this.innerNgModel && this.innerNgModel.errors) || null;
    }

}

和模板(custom-datepicker.compnent.html):

 <mat-form-field>
    <mat-label *ngIf="label">{{ label }}</mat-label>
    <input matInput
        #innerNgModel="ngModel"
        [matDatepicker]="#picker"
        [(ngModel)]="modelValue"
        (ngModelChange)="modelChange($event)"
        [disabled]="disabled"
        [required]="required"
        [placeholder]="placeholder"
        [min]="min"
        [max]="max">
    <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
    <mat-datepicker #picker></mat-datepicker>
    <mat-error *ngIf="innerNgModel?.errors?.required">This field is required!</mat-error>
    <mat-error *ngIf="innerNgModel?.errors?.matDatepickerMin">Date is too early!</mat-error>
    <mat-error *ngIf="innerNgModel?.errors?.matDatepickerMax">Date is too late!</mat-error>
</mat-form-field>
 

周围的微模块(custom-datepicker.module.ts):

 import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {MatDatepickerModule, MatFormFieldModule, MatInputModule, MAT_DATE_LOCALE, MAT_DATE_FORMATS} from '@angular/material';
import {CustomDatepickerComponent} from './custom-datepicker.component';
import {MAT_MOMENT_DATE_ADAPTER_OPTIONS, MatMomentDateModule} from '@angular/material-moment-adapter';
import {CommonModule} from '@angular/common';

const DATE_FORMATS = {
    parse: {dateInput: 'YYYY MM DD'},
    display: {dateInput: 'YYYY.MM.DD', monthYearLabel: 'MMM YYYY', dateA11yLabel: 'LL', monthYearA11yLabel: 'MMMM YYYY'}
};

@NgModule({
    imports: [
        CommonModule,
        FormsModule,
        MatMomentDateModule,
        MatFormFieldModule,
        MatInputModule,
        MatDatepickerModule
    ],
    declarations: [
        CustomDatepickerComponent
    ],
    exports: [
        CustomDatepickerComponent
    ],
    providers: [
        {provide: MAT_DATE_LOCALE, useValue: 'es-ES'},
        {provide: MAT_DATE_FORMATS, useValue: DATE_FORMATS},
        {provide: MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: {useUtc: false}}
    ]
})
export class CustomDatepickerModule {}
 

以及部分外部表单组件:

 <form #outerForm="ngForm" (ngSubmit)="submitForm(outerForm)">
    ...
    <custom-datepicker [(ngModel)]="myDate" #date="ngModel"></custom-datepicker>
    <pre>{{ date.errors | json }}</pre>
    <button (click)="myDate = null">set2null</button>
    ...
 

解决方案

我遇到了相同的任务,并且在处理本地模型的绑定和更改时采用了不同的方法.

我没有分离并手动设置ngModelChange回调,而是将局部变量隐藏在一对称为getter的getter \ setters后面.

在您的情况下,代码如下所示:

custom-datepicker.component.html中:

<input matInput
        #innerNgModel="ngModel"
        [matDatepicker]="#picker"
        [(ngModel)]="modelValue"
        [disabled]="disabled"
        [required]="required"
        [placeholder]="placeholder"
        [min]="min"
        [max]="max">

custom-datepicker.component.ts期间:

  get modelValue(){
      return this._modelValue;
  }

  set modelValue(newValue){
     if(this._modelValue != newValue){
          this._modelValue = newValue;
          this.emitChange();
     }
  }

  public writeValue(data) {
        this.modelValue = data ? moment(data) : null;
  }

您可以在 https:中查看实际组件://github.com/cdigruttola/GestioneTessere/tree/master/Server/frontend/src/app/viewedit

我不知道它是否会有所作为,但是我在测试应用程序时在验证处理方面没有发现任何问题,并且实际用户都没有向我报告过.

To show a kind of real world example, let's say that we want to use the @angular/material's datepicker in our application.

We want to use it on a lot of pages, so we want to make it very easy to add it to a form with the same configuration everywhere. To fulfill this need, we create a custom angular component around a <mat-datepicker> with ControlValueAccessor implementation to be able to use [(ngModel)] on it.

We want to handle the typical validations in the component, but in the same time, we want to make the result of the validation available for the outer component that includes our CustomDatepickerComponent.

As an easy solution, we can implement the validate() method like this (innerNgModel comes from exported ngModel: #innerNgModel="ngModel". See full code at the end of this question):

validate() {
    return (this.innerNgModel && this.innerNgModel.errors) || null;
}

At this point we can use a datepicker in any form component in a very simple way (as we wanted):

<custom-datepicker [(ngModel)]="myDate"></custom-datepicker>

We can also extend the above line to have a better debug experience (like this):

<custom-datepicker [(ngModel)]="myDate" #date="ngModel"></custom-datepicker>
<pre>{{ date.errrors | json }}</pre>

As long as I'm changing the value in the custom datepicker component, everything works fine. The surrounding form remains invalid if the datepicker has any errors (and it becomes valid if the datepicker is valid).

BUT!

If the myDate member of the outer form component (the one is passed as ngModel) is changed by the outer component (like: this.myDate= null), then the following happens:

  1. The writeValue() of the CustomDatepickerComponent runs, and it updates the value of the datepicker.
  2. The validate() of the CustomDatepickerComponent runs, but at this point the innerNgModel is not updated so it returns the validation of an earlier state.

To solve this issue, we can emit a change from the component in a setTimeout:

public writeValue(data) {
    this.modelValue = data ? moment(data) : null;
    setTimeout(() => { this.emitChange(); }, 0);
}

In this case, the emitChange (broadcasts change of the custom comoponent) is going to trigger a new validation. And because of the setTimeout, it is going to run in the next cycle when the innerNgModel is updated already.


My question is that if there is any better way to handle this issue than using setTimeout? And if possible, I would stick to template driven implementation.

Thanks in advance!


Full source code of the example:

custom-datepicker.component.ts

import {Component, forwardRef, Input, ViewChild} from '@angular/core';
import {ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, NgModel} from '@angular/forms';
import * as moment from 'moment';
import {MatDatepicker, MatDatepickerInput, MatFormField} from '@angular/material';
import {Moment} from 'moment';

const AC_VA: any = {
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => CustomDatepickerComponent),
    multi: true
};

const VALIDATORS: any = {
    provide: NG_VALIDATORS,
    useExisting: forwardRef(() => CustomDatepickerComponent),
    multi: true,
};

const noop = (_: any) => {};

@Component({
    selector: 'custom-datepicker',
    templateUrl: './custom-datepicker.compnent.html',
    providers: [AC_VA, VALIDATORS]
})
export class CustomDatepickerComponent implements ControlValueAccessor {

    constructor() {}

    @Input() required: boolean = false;
    @Input() disabled: boolean = false;
    @Input() min: Date = null;
    @Input() max: Date = null;
    @Input() label: string = null;
    @Input() placeholder: string = 'Pick a date';

    @ViewChild('innerNgModel') innerNgModel: NgModel;

    private propagateChange = noop;

    public modelChange(event) {
        this.emitChange();
    }

    public writeValue(data) {
        this.modelValue = data ? moment(data) : null;
        setTimeout(() => { this.emitChange(); }, 0);
    }

    public emitChange() {
        this.propagateChange(!this.modelValue ? null : this.modelValue.toDate());
    }

    public registerOnChange(fn: any) { this.propagateChange = fn; }

    public registerOnTouched() {}

    validate() {
        return (this.innerNgModel && this.innerNgModel.errors) || null;
    }

}

And the template (custom-datepicker.compnent.html):

<mat-form-field>
    <mat-label *ngIf="label">{{ label }}</mat-label>
    <input matInput
        #innerNgModel="ngModel"
        [matDatepicker]="#picker"
        [(ngModel)]="modelValue"
        (ngModelChange)="modelChange($event)"
        [disabled]="disabled"
        [required]="required"
        [placeholder]="placeholder"
        [min]="min"
        [max]="max">
    <mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
    <mat-datepicker #picker></mat-datepicker>
    <mat-error *ngIf="innerNgModel?.errors?.required">This field is required!</mat-error>
    <mat-error *ngIf="innerNgModel?.errors?.matDatepickerMin">Date is too early!</mat-error>
    <mat-error *ngIf="innerNgModel?.errors?.matDatepickerMax">Date is too late!</mat-error>
</mat-form-field>

The surrounding micro-module (custom-datepicker.module.ts):

import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {MatDatepickerModule, MatFormFieldModule, MatInputModule, MAT_DATE_LOCALE, MAT_DATE_FORMATS} from '@angular/material';
import {CustomDatepickerComponent} from './custom-datepicker.component';
import {MAT_MOMENT_DATE_ADAPTER_OPTIONS, MatMomentDateModule} from '@angular/material-moment-adapter';
import {CommonModule} from '@angular/common';

const DATE_FORMATS = {
    parse: {dateInput: 'YYYY MM DD'},
    display: {dateInput: 'YYYY.MM.DD', monthYearLabel: 'MMM YYYY', dateA11yLabel: 'LL', monthYearA11yLabel: 'MMMM YYYY'}
};

@NgModule({
    imports: [
        CommonModule,
        FormsModule,
        MatMomentDateModule,
        MatFormFieldModule,
        MatInputModule,
        MatDatepickerModule
    ],
    declarations: [
        CustomDatepickerComponent
    ],
    exports: [
        CustomDatepickerComponent
    ],
    providers: [
        {provide: MAT_DATE_LOCALE, useValue: 'es-ES'},
        {provide: MAT_DATE_FORMATS, useValue: DATE_FORMATS},
        {provide: MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: {useUtc: false}}
    ]
})
export class CustomDatepickerModule {}

And parts of the outer form component:

<form #outerForm="ngForm" (ngSubmit)="submitForm(outerForm)">
    ...
    <custom-datepicker [(ngModel)]="myDate" #date="ngModel"></custom-datepicker>
    <pre>{{ date.errors | json }}</pre>
    <button (click)="myDate = null">set2null</button>
    ...

解决方案

I have faced the same task and I have taken a different approach in handling binding and change of the local model.

Instead of separating and manually setting an ngModelChange callback, I have hidden my local variable behind a pair of getter\setters, where my callback is called.

In your case, the code would look like this:

in custom-datepicker.component.html:

<input matInput
        #innerNgModel="ngModel"
        [matDatepicker]="#picker"
        [(ngModel)]="modelValue"
        [disabled]="disabled"
        [required]="required"
        [placeholder]="placeholder"
        [min]="min"
        [max]="max">

while in custom-datepicker.component.ts:

  get modelValue(){
      return this._modelValue;
  }

  set modelValue(newValue){
     if(this._modelValue != newValue){
          this._modelValue = newValue;
          this.emitChange();
     }
  }

  public writeValue(data) {
        this.modelValue = data ? moment(data) : null;
  }

You can see the actual component in https://github.com/cdigruttola/GestioneTessere/tree/master/Server/frontend/src/app/viewedit

I don't know if it will make a difference, but I have seen no problem in validation handling while I was testing the application and none has been reported to me by the actual users.

这篇关于Angular自定义组件中的不一致验证问题的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆