Angular ReactiveForms:ControlValueAccessor 在不通知更改的情况下验证初始值

Angular ReactiveForms: ControlValueAccessor validate initial value without notifying change

提问人:Andrea Moretti 提问时间:11/9/2023 更新时间:11/9/2023 访问量:42

问:

我试图找到一些解决方法或解决方案,但没有结果。 我正在尝试实现 CustomValueAccessor,它可以验证给定控件的初始值,如果没有,则在不通知对父控件的更改的情况下更改它,而是更改其值。

问题出在 ngOnInit 循环中。如果我调用此代码:

if (!this._initialIsValid) {
      this.onChange(this.period);
      this.ngControl.control?.markAsPristine();
}

控件的值在内部和父级中都发生了更改,但我在父级上收到通知。 如果我不调用 ,则该值仅在控件内部更改,因此我不会收到通知,但父级仍具有初始值。 我不需要通知更改,因为在父组件中,我订阅并触发了一些 http 请求。formGrouponChangeformGroupformGroup.valueChanges()

我还尝试过:

if (!this._initialIsValid) {
      this.ngControl.control?.setValue(this.period, {emitEvent: false});
      this.ngControl.control?.markAsPristine();
}

但它的工作方式与并且仍然有通知。onChange

基本上,我正在实现月年选择器(使用 Angular Material),以便能够在将更改发送到父控件之前选择月年并将其转换为 yyyyMM 格式。我有 dateFilter 函数,允许 datepicker 禁用不可用的日期。 验证函数基本上检查数字数组是否包含 yyyyMM 给定的周期。如果数组为空,则每个日期都被视为有效。

基本句点选取器-html

<mat-form-field [ngClass]="fullwidth ? 'w100' : ''">
    <mat-label>{{ label }}</mat-label>
    <input
      [placeholder]="label"
      matInput
      [matDatepicker]="dp"
      readonly
      [required]="required"
      [value]="inputDate"
      [disabled]="disabled"
      [matDatepickerFilter]="dateFilter"
    />
    <!-- <mat-hint>MM/YYYY</mat-hint> -->
    <button
      mat-icon-button
      matIconPrefix
      *ngIf="allowClear && period > 0"
      (click)="clearPeriod(dp)"
    >
      <mat-icon>cancel</mat-icon>
    </button>
    <mat-datepicker-toggle matIconSuffix [for]="dp"></mat-datepicker-toggle>
    <mat-datepicker
      #dp
      disabled="false"
      startView="multi-year"
      (monthSelected)="setMonthAndYear($event, dp)"
      panelClass="month-picker"
    >
    </mat-datepicker>
    <mat-error *ngIf="required">Campo obbligatorio</mat-error>
  </mat-form-field>
  

basic-period-picker.component.ts

import { Component, Input, Optional, Self, ViewEncapsulation } from '@angular/core';
import { ControlValueAccessor, NgControl } from '@angular/forms';
import { MAT_DATE_FORMATS } from '@angular/material/core';
import { MatDatepicker } from '@angular/material/datepicker';
import { OWTAM_PERIOD_FORMATS } from '../period-date-format';
import { PeriodHelper } from '../period-helper';
import { DateTime } from 'luxon';
import * as _ from 'lodash';

@Component({
  selector: 'owtam-basic-period-picker',
  templateUrl: './basic-period-picker.component.html',
  styleUrls: ['./basic-period-picker.component.scss'],
  providers: [
    // { provide: NG_VALUE_ACCESSOR, useExisting: BasicPeriodPickerComponent, multi: true },
    { provide: MAT_DATE_FORMATS, useValue: OWTAM_PERIOD_FORMATS }
  ],
  encapsulation: ViewEncapsulation.None,
})
export class BasicPeriodPickerComponent implements ControlValueAccessor {

  @Input()
  label: string = "Periodo";

  @Input()
  allowClear: boolean = true;

  @Input()
  required: boolean = false;

  @Input()
  disabled: boolean = false;

  @Input()
  period!: number;

  @Input()
  openCalendarOnClear: boolean = true;

  @Input()
  fullwidth: boolean = true;

  @Input()
  validPeriods: number[] = [];

  inputDate!: DateTime | undefined;

  ngControl!: NgControl;
  constructor(
    @Optional() @Self() ngControl: NgControl
  ) {
    if (ngControl) {
      this.ngControl = ngControl;
      this.ngControl.valueAccessor = this;
    }
  }

  private _initialValidation: boolean = true;
  private _initialIsValid: boolean = false;

  onChange!: (date: number) => void;

  onTouched!: () => void;

  writeValue(period: number): void {
    const value = this._getValidValue(period);
    if (this._initialValidation) {
      this._initialIsValid = _.isEqual(period, value);
    }
    this.period = value;
    this.inputDate = PeriodHelper.periodToDate(this.period);
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState?(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  ngOnInit() {
    // if (this.ngControl.control) {
    //   this.ngControl.control.setValue(this.period);
    //   //  { onlySelf: false, emitModelToViewChange: false, emitViewToModelChange: false, emitEvent: false }
    // }
    console.log('initialIsValid: ', this._initialIsValid);
    if (!this._initialIsValid) {
      this.onChange(this.period);
      this.ngControl.control?.markAsPristine();
    }
    this._initialValidation = false;
  }

  dateFilter = (d: DateTime | null): boolean => {
    return PeriodHelper.periodsFilter(this.validPeriods, d);
  }

  // Evento scatenato alla fine della selezione (anno e mese)
  // Viene utilizzata la classe globale OWGlobal per trasformare le date in IDPeriodo e viceversa
  setMonthAndYear(date: DateTime, datepicker: MatDatepicker<DateTime>) {
    this.inputDate = date;
    this.period = PeriodHelper.dateToPeriod(date);
    this.onChange(this.period);
    datepicker.close();
  }

  clearPeriod(dp: MatDatepicker<DateTime>) {
    this.period = 0;
    this.inputDate = undefined;
    this.onChange(0);
    if (this.openCalendarOnClear) {
      dp.open();
    }
  }

  private _getValidValue(period: number) {
    if (period == 0) {
      period = PeriodHelper.currentPeriod();
    }
    if (this.validPeriods && this.validPeriods.length > 0) {
      if (this.validPeriods.length === 1) {
        period = this.validPeriods[0];
      } else {
        const isValid = this.validPeriods.includes(period);
        if (!isValid) {
          const closest = this.validPeriods
            .map(p => { return { diff: Math.abs(period - p), value: p } })
            .sort((a, b) => a.diff - b.diff)[0].value;
          period = closest;
        }
      }
    }
    return period;
  }
}

period-helper.ts

import { DateTime } from "luxon";

export class PeriodHelper {
    public static dateToPeriod(date: DateTime) {
        if (date) {
            const year = date.year;
            const month = date.month; // Mese inizia da 0, aggiungiamo 1 per ottenere il mese corretto
            const yearMonth = year * 100 + month; // Componiamo il numero yyyyMM
    
            return yearMonth;
        }
        return 0;
    }

    // Da periodo [YYYYMM] a data
    public static periodToDate(period: number): DateTime | undefined {
        if (period) {
            const year = Math.floor(period / 100); // Ottieni l'anno dividendo per 100 e arrotondando verso il basso
            const month = period % 100; // Ottieni il mese prendendo il resto della divisione per 100
            const date = DateTime.fromObject({year, month, day: 1}); // Sottrai 1 al mese perché inizia da 0, quindi il primo mese è 0 (Gennaio)
    
            return date;
        }

        return undefined;
    }

    public static currentPeriod() {
        return this.dateToPeriod(DateTime.now());
    }

    public static periodsFilter(validPeriods: number[], d: DateTime | null): boolean {
        if (d) {
          if (validPeriods && validPeriods.length > 0) {
            const p = PeriodHelper.dateToPeriod(d);
            return validPeriods.includes(p);
          }
        }
        return true;
      }
}

component.ts

validPeriods = [201804, 202205, 202206, 202207, 202401, 202501];

filterForm = new FormGroup({
    //you can try even with PeriodHelper.currentPeriod() as initial value
    period: new FormControl<number>(0, {nonNullable: true}),
  })

组件:.html

<div [formGroup]="filterForm">
<owtam-basic-period-picker
    [validPeriods]="validPeriods"
    formControlName="period"
    [allowClear]="false"
  >
</div>

我想在组件中进行此验证,因为我将在我的应用程序中重用很多,因此我无法为每个父级进行验证。

对不起,我的英语不好

提前致谢

反应式形式 angular-forms angular16 controlvalueaccessor

评论


答: 暂无答案