Непоследовательная проблема проверки в пользовательском компоненте Angular

Чтобы показать пример из реальной жизни, допустим, что мы хотим использовать указатель даты @ 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>

Пока я изменяю значение в пользовательском компоненте DatePicker, все работает отлично. Окружающая форма остается недействительной, если в указателе даты есть какие-либо ошибки (и она становится действительной, если указатель даты действителен).

НО!

Если myDate элемент внешнего компонента формы (который передается как ngModel) изменяется внешним компонентом (например: this.myDate= null), то происходит следующее:

  1. writeValue() CustomDatepickerComponent запускается, и он обновляет значение DatePicker.
  2. validate() CustomDatepickerComponent запускается, но на этом этапе innerNgModel не обновляется, поэтому возвращает подтверждение более раннего состояния.

Чтобы решить эту проблему, мы можем передать изменение из компонента в setTimeout:

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

В этом случае emitChange (транслирует изменение пользовательского компонента) запускает новую проверку. И из-за setTimeout, он будет работать в следующем цикле, когда innerNgModel уже обновлен.


Мой вопрос заключается в том, что если есть лучший способ справиться с этой проблемой, чем с помощью setTimeout? И если возможно, я бы придерживался реализации, управляемой шаблоном.

Заранее спасибо!


Полный исходный код примера:

таможенно-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>
    ...

0 ответов

Я столкнулся с той же задачей, и я принял другой подход в обработке привязки и изменения локальной модели.

Вместо разделения и ручной настройки ngModelChange обратный вызов, я спрятал свою локальную переменную за парой 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

Я не знаю, будет ли это иметь значение, но я не видел проблем с обработкой валидации во время тестирования приложения, и фактические пользователи не сообщили мне ни об одном.

Другие вопросы по тегам