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 }
问题原因分析
- 状态同步不一致:主输入框的
ngModel绑定依赖selectedDateRange,但自定义输入框修改后未同步更新daterangepicker组件内部的options配置,导致失焦时组件读取初始默认值回显。 - 类型混乱:
selectedDateRange有时被存储为格式化后的字符串,有时是包含moment对象的对象,导致渲染时出现匹配错误。 - 手动调用
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]="{
相关产品推荐
相关产品推荐

