【问题标题】:Inconsistent validation issue in Angular custom componentAngular 自定义组件中的不一致验证问题
【发布时间】:2019-04-05 15:43:33
【问题描述】:

为了展示一个真实世界的例子,假设我们想在我们的应用程序中使用@angular/material 的日期选择器。

我们想在很多页面上使用它,所以我们想让它很容易地添加到具有相同配置的表单中。为了满足这一需求,我们在 <mat-datepicker> 周围创建了一个自定义角度组件,并带有 ControlValueAccessor 实现,以便能够在其上使用 [(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>

只要我更改自定义日期选择器组件中的值,一切正常。如果日期选择器有任何错误,则周围的表单仍然无效(如果日期选择器有效,则它变为有效)。

但是!

如果外部表单组件的 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>
    ...

【问题讨论】:

  • 我不会把这个作为答案,因为我目前正在研究这个时序问题的解决方案,但它还没有完成。我正在做的是创建一个与正在使用的事件的状态挂钩的设置器,以便在事件发生时触发设置器以进行清理/验证。
  • 我会考虑使用 formcontrol 而不是 ng 模型。 angular.io/api/forms/FormControl
  • 在我看来没有必要将 datepicker 包装到另一个组件中。您节省了几个字节,但您失去了灵活性并增加了复杂性。您可以将错误消息包装到某个组件中,这样就足够了...
  • 我知道有可能使用反应形式。我的问题是关于模板驱动的表单实现。 @smnbbrv:包装日期选择器不是为了节省字节。除了这个例子之外,还有一个更复杂的自定义日期选择器表单部分的实现。它的一个非常重要的优点是我们可以在一个健壮的应用程序的所有形式中简单地使用相同的实现。将此实现复制粘贴到每个表单中将是一种非常糟糕的做法。
  • 你说得对,我没有提供。但我故意跳过它以使示例更简单。编写材料自定义表单字段控件也可能是一个很好的答案,但我的问题想要更笼统。 mat-datepicker 可以是任何类型的实现 ControlValueAccessor 的第 3 方组件。抱歉,如果问题不清楚!

标签: javascript angular angular-material angular-validation controlvalueaccessor


【解决方案1】:

我也面临同样的任务,但我采取了不同的方法来处理本地模型的绑定和更改。

我没有分离和手动设置 ngModelChange 回调,而是将我的局部变量隐藏在一对 getter\setter 后面,我的回调被调用。

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

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看到实际的组件

我不知道这是否会有所不同,但我在测试应用程序时发现验证处理没有问题,实际用户没有向我报告任何问题。

【讨论】:

    猜你喜欢
    • 2016-09-28
    • 2016-05-06
    • 2020-07-14
    • 1970-01-01
    • 2013-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-05
    相关资源
    最近更新 更多