You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Angular ng2-daterangepicker自定义输入框失焦值回退问题求助

Angular自定义ng2-daterangepicker组件:主输入框失焦值回退问题

我是Angular新手,在自定义ng2-daterangepicker组件时新增了开始、结束日期输入框,功能虽能运行,但主输入框失焦后会回退至options中设置的默认值。目前表单FormGroup中已能获取新输入值,但UI仍显示旧值,需实现输入值实时更新且不回退的效果。

现有代码

daterange-input.component.ts

import {
  Component,
  forwardRef,
  Input,
  OnInit,
  Renderer2
} from '@angular/core';
import {AbstractControl, ControlValueAccessor, NG_VALUE_ACCESSOR} from "@angular/forms";
import * as moment from "moment";
import {ValidationService} from "../../../services/validation/validation.service";

@Component({
  selector: 'app-daterange-input',
  templateUrl: './daterange-input.component.html',
  styleUrls: ['./daterange-input.component.scss'],
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => DaterangeInputComponent),
      multi: true
    }
  ]
})

export class DaterangeInputComponent implements ControlValueAccessor, OnInit {
  @Input() options: any;
  @Input() name: string;
  @Input() coverClass: string;
  @Input() labelClass: string;
  @Input() inputClass: string;
  @Input() labelText: string;
  @Input() inputDivCover: string;
  @Input() control: AbstractControl | null;

  selectedDateRange: any;
  dateFormat: string;

  created: boolean;
  startInputValue: string | null = null;
  endInputValue: string | null = null;

  private onChange: (value: any) => void = () => {};
  private onTouched: () => void = () => {};

  constructor(
    public validationService: ValidationService,
    private renderer: Renderer2
  ) {
    this.created = false;
    this.dateFormat = 'MM/DD/YYYY'
    this.name = "";
    this.coverClass = "form-inline";
    this.labelClass = "";
    this.inputClass = "form-control-styled form-control";
    this.labelText = "";
    this.inputDivCover = "";
    this.control = null;
  }

  ngOnInit() {
    if (this.options.startDate && this.options.endDate) {
      this.updateValueCover(this.options.startDate, this.options.endDate)
    }
    document.addEventListener('click', this.onDocumentClick.bind(this));
  }

  ngOnDestroy() {
    document.removeEventListener('click', this.onDocumentClick.bind(this));
  }

  onDocumentClick(event: Event) {
    const target = event.target as HTMLElement;
    const isDaterangepickerInput = target.matches('#' + this.name) || target.parentElement?.matches('#' + this.name);

    if (isDaterangepickerInput && !this.created) {
      this.created = true;
      this.createAdditionalElements();
    }
  }

  updateValueCover(start: any, end: any) {
    const startDate = moment(start, this.dateFormat);
    const endDate = moment(end, this.dateFormat);
    setTimeout(() => {
      this.selectedDateRange = {start: startDate, end: endDate};
      if (this.onChange) {
        this.onChange(this.formatDateRange(this.selectedDateRange));
      }
      if (this.onTouched) {
        this.onTouched();
      }
    });
  }

  createAdditionalElements() {
    const leftCalendarDiv = this.renderer.createElement('div');
    this.renderer.addClass(leftCalendarDiv, 'drp-calendar');
    this.renderer.addClass(leftCalendarDiv, 'left');
    const leftInput = this.renderer.createElement('input');
    this.renderer.setAttribute(leftInput, 'type', 'text');
    this.renderer.setAttribute(leftInput, 'name', this.name + '-start');
    this.renderer.setAttribute(leftInput, 'id', this.name + '-start');
    this.renderer.listen(leftInput, 'change', (event: Event) => this.onStartDateChange((event.target as HTMLInputElement).value));
    this.renderer.setProperty(leftInput, 'value', this.selectedDateRange?.start?.format(this.dateFormat));
    this.renderer.appendChild(leftCalendarDiv, leftInput);

    const rightCalendarDiv = this.renderer.createElement('div');
    this.renderer.addClass(rightCalendarDiv, 'drp-calendar');
    this.renderer.addClass(rightCalendarDiv, 'right');
    const rightInput = this.renderer.createElement('input');
    this.renderer.setAttribute(rightInput, 'type', 'text');
    this.renderer.setAttribute(rightInput, 'name', this.name + '-end');
    this.renderer.setAttribute(rightInput, 'id', this.name + '-end');
    this.renderer.listen(rightInput, 'change', (event: Event) => this.onEndDateChange((event.target as HTMLInputElement).value));
    this.renderer.setProperty(rightInput, 'value', this.selectedDateRange?.end?.format(this.dateFormat));
    this.renderer.appendChild(rightCalendarDiv, rightInput);

    this.renderer.addClass(leftInput, "form-control-styled");
    this.renderer.addClass(rightInput, "form-control-styled");

    const calendarLeftElement = document.body.querySelector('.drp-calendar.left');
    const calendarRightElement = document.body.querySelector('.drp-calendar.right');

    this.renderer.appendChild(calendarLeftElement, leftCalendarDiv);
    this.renderer.appendChild(calendarRightElement, rightCalendarDiv);
  }

  onStartDateChange(value: string) {
    this.startInputValue = value;
    this.updateDateRange();
  }

  onEndDateChange(value: string) {
    this.endInputValue = value;
    this.updateDateRange();
  }

  updateDateRange() {
    if (this.startInputValue && this.endInputValue) {
      this.updateValueCover(this.startInputValue, this.endInputValue)
      this.writeValue({start: this.startInputValue, end:this.endInputValue})
    }
  }

  get controlElement() {
    return this.control;
  }

  onDateSelected(dateRange: any) {
    this.selectedDateRange = this.formatDateRange(dateRange);
    if (this.onChange) {
      this.onChange(this.formatDateRange(dateRange));
    }
    if (this.onTouched) {
      this.onTouched();
    }
  }

  formatDateRange(dateRange: any): string {
    if (dateRange?.start && dateRange?.end) {
      const startFormatted = dateRange.start.format(this.dateFormat);
      const endFormatted = dateRange.end.format(this.dateFormat);
      return this.options.singleDatePicker ? `${startFormatted}` : `${startFormatted} - ${endFormatted}`;
    }
    return dateRange;
  }

  updateValue(value: any) {
    this.selectedDateRange = this.formatDateRange(value);

    if (this.onChange) {
      this.onChange(this.formatDateRange(value));
    }
    if (this.onTouched) {
      this.onTouched();
    }
  }

  writeValue(value: any): void {
    if (value && value.start && value.end) {
      this.selectedDateRange = {
        start: moment(value.start, this.dateFormat),
        end: moment(value.end, this.dateFormat)
      };
      setTimeout(() => {
        if (this.onChange) {
          this.onChange(this.formatDateRange(this.selectedDateRange));
        }
        if (this.onTouched) {
          this.onTouched();
        }
      });
    }
  }

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

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

  setDisabledState(isDisabled: boolean): void {}

  onBlurSelected() {
    if (!this.selectedDateRange) {
      this.onDateSelected({
        start: moment(),
        end: moment(),
        label: null
      })
    }
  }
}

daterange-input.component.html

<div class="{{coverClass}}">
  <label class="{{labelClass}}" [htmlFor]="name">{{labelText}}</label>
  <div class="{{inputDivCover}}">
    <input
      type="text"
      [name]="name"
      [id]="name"
      daterangepicker
      class="{{inputClass}}"
      [options]="options"
      (selected)="onDateSelected($event)"
      (blur)="onBlurSelected()"
      [ngClass]="{'single-datepicker': options.singleDatePicker}"
      [ngModel]="formatDateRange(selectedDateRange)"
      [class.ng-invalid]="!validationService.isValid(controlElement)"
      [class.ng-valid]="validationService.isValid(controlElement)"
      [class.ng-touched]="controlElement!.touched"
      (ngModelChange)="updateValue($event)">
  </div>
  <error-field [fieldToValidate]="{meta:controlElement, name:name}"></error-field>
</div>

组件使用方式

<div class="col-33 pr-lg">
  <app-daterange-input
    [options]="dateRangeProps"
    formControlName="poDate"
    [control]="filters.get('poDate')"
    [labelText]="'PO Submit Date'"
    [labelClass]="'flex-basis-100 w-50-i margin-unset'"
    [inputDivCover]="'date wr-datepicker w-50-i'"
    [name]="'poDate'"
  ></app-daterange-input>
</div>

filters表单组

this.filters = new FormGroup({
  client: new FormControl("", [Validators.required]),
  contract: new FormControl({value:"", disabled: true}, [Validators.required]),
  poDate: new FormControl("", [Validators.required]),
})

dateRangeProps配置

{
  "showDropdowns": true,
  "ranges": {
    "Today": [
      "2023-07-21T17:29:19.967Z",
      "2023-07-21T17:29:19.967Z"
    ],
    "Yesterday": [
      "2023-07-20T17:29:19.967Z",
      "2023-07-20T17:29:19.968Z"
    ],
    "This Week": [
      "2023-07-15T21:00:00.000Z",
      "2023-07-22T20:59:59.999Z"
    ],
    "Last Week": [
      "2023-07-08T21:00:00.000Z",
      "2023-07-15T20:59:59.999Z"
    ],
    "Last 7 Days": [
      "2023-07-15T17:29:19.969Z",
      "2023-07-21T17:29:19.969Z"
    ],
    "Last 30 Days": [
      "2023-06-22T17:29:19.969Z",
      "2023-07-21T17:29:19.969Z"
    ],
    "This Month": [
      "2023-06-30T21:00:00.000Z",
      "2023-07-31T20:59:59.999Z"
    ],
    "Last Month": [
      "2023-05-31T21:00:00.000Z",
      "2023-06-30T20:59:59.999Z"
    ],
    "This Year": [
      "2022-12-31T22:00:00.000Z",
      "2023-12-31T21:59:59.999Z"
    ],
    "All": [
      "1899-12-31T21:57:56.000Z",
      "2500-12-30T22:00:00.000Z"
    ]
  },
  "showCustomRangeLabel": true,
  "alwaysShowCalendars": true,
  "autoApply": true,
  "startDate": "07/10/2023",
  "endDate": "07/15/2023",
  "singleDatePicker": false
}

问题原因分析

  1. 状态同步不一致:主输入框的ngModel绑定依赖selectedDateRange,但自定义输入框修改后未同步更新daterangepicker组件内部的options配置,导致失焦时组件读取初始默认值回显。
  2. 类型混乱:selectedDateRange有时被存储为格式化后的字符串,有时是包含moment对象的对象,导致渲染时出现匹配错误。
  3. 手动调用writeValue:writeValue是表单控件触发的方法,手动调用会打破Angular表单的双向绑定逻辑,导致UI状态异常。

解决方案

1. 统一selectedDateRange类型

修改onDateSelected方法,直接存储moment对象,避免转成字符串:

onDateSelected(dateRange: any) {
  this.selectedDateRange = {
    start: moment(dateRange.start),
    end: moment(dateRange.end)
  };
  this.onChange(this.formatDateRange(this.selectedDateRange));
  this.onTouched();
}

2. 移除updateDateRange中手动调用的writeValue

updateDateRange() {
  if (this.startInputValue && this.endInputValue) {
    this.updateValueCover(this.startInputValue, this.endInputValue);
    // 移除该行:this.writeValue({start: this.startInputValue, end:this.endInputValue})
  }
}

3. 同步更新options中的日期值

在updateValueCover中同步修改options的startDate和endDate,避免失焦时回退初始值:

updateValueCover(start: any, end: any) {
  const startDate = moment(start, this.dateFormat);
  const endDate = moment(end, this.dateFormat);
  // 同步更新options
  this.options.startDate = startDate.format(this.dateFormat);
  this.options.endDate = endDate.format(this.dateFormat);
  
  this.selectedDateRange = {start: startDate, end: endDate};
  this.onChange(this.formatDateRange(this.selectedDateRange));
  this.onTouched();
  // 移除不必要的setTimeout
}

4. 用模板替代动态创建自定义输入框

修改HTML模板,直接添加自定义输入框,通过ngIf控制显示,实现双向绑定:

<div class="{{coverClass}}">
  <label class="{{labelClass}}" [htmlFor]="name">{{labelText}}</label>
  <div class="{{inputDivCover}}">
    <input
      type="text"
      [name]="name"
      [id]="name"
      daterangepicker
      class="{{inputClass}}"
      [options]="options"
      (selected)="onDateSelected($event)"
      (blur)="onBlurSelected()"
      [ngClass]="{'single-datepicker': options.singleDatePicker}"
      [ngModel]="formatDateRange(selectedDateRange)"
      [class.ng-invalid]="!validationService.isValid(controlElement)"
      [class.ng-valid]="validationService.isValid(controlElement)"
      [class.ng-touched]="controlElement!.touched"
      (ngModelChange)="updateValue($event)">
    <!-- 自定义输入框模板 -->
    <div *ngIf="created" class="drp-calendar-group">
      <div class="drp-calendar left">
        <input 
          type="text" 
          [name]="name + '-start'"
          [id]="name + '-start'"
          class="form-control-styled"
          [(ngModel)]="startInputValue"
          (change)="onStartDateChange(startInputValue)">
      </div>
      <div class="drp-calendar right">
        <input 
          type="text" 
          [name]="name + '-end'"
          [id]="name + '-end'"
          class="form-control-styled"
          [(ngModel)]="endInputValue"
          (change)="onEndDateChange(endInputValue)">
      </div>
    </div>
  </div>
  <error-field [fieldToValidate]="{
相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 23:18:30