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

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

问题描述

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

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

我们想在很多页面上使用它,所以我们想让它很容易地添加到一个到处都具有相同配置的表单中.为了满足这一需求,我们围绕带有 ControlValueAccessor 实现的 创建了一个自定义角度组件,以便能够在其上使用 [(ngModel)].

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.

我们希望处理组件中的典型验证,但同时,我们希望将验证结果提供给包含我们的 CustomDatepickerComponent 的外部组件.

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.

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

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>

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

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).

但是!

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

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. CustomDatepickerComponent 的 writeValue() 运行,并更新日期选择器的值.
  2. CustomDatepickerComponent 的 validate() 运行,但此时 innerNgModel 未更新,因此它返回早期状态的验证.
  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.

为了解决这个问题,我们可以在 setTimeout 中从组件发出更改:

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);
}

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

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.

我的问题是,是否有比使用 setTimeout 更好的方法来处理这个问题?如果可能的话,我会坚持模板驱动的实现.

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.

提前致谢!

示例的完整源代码:

custom-datepicker.component.ts

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):

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>

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

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 {}

和部分外型组件:

<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.

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

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:

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/GestineTessere/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天全站免登陆