Skip to content

Commit 6193c38

Browse files
committed
fix(input-directive): input does not remove ng-invalid with valid model value
When the user enters an invalid date (i.e. past date or total non-sense) the input validates and adds 'ng-invalid' to the input. Then the model is updated to a valid input (i.e. not through the input but rather the date-picker or an async call returning a value) the directive correctly sets `ng-valid` on the input. Prior to this fix, the input would not re-validate when changes to the model happened. Fix #448
1 parent ceb404a commit 6193c38

File tree

3 files changed

+81
-33
lines changed

3 files changed

+81
-33
lines changed

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/dl-date-time-input/dl-date-time-input.directive.ts

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Directive, ElementRef, EventEmitter, forwardRef, HostListener, Inject, Input, Output, Renderer2} from '@angular/core';
1+
import {Directive, ElementRef, EventEmitter, HostListener, Inject, Input, Output, Renderer2} from '@angular/core';
22
import {
33
AbstractControl,
44
ControlValueAccessor,
@@ -27,16 +27,16 @@ const moment = _moment;
2727
@Directive({
2828
selector: 'input[dlDateTimeInput]',
2929
providers: [
30-
{provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DlDateTimeInputDirective), multi: true},
31-
{provide: NG_VALIDATORS, useExisting: forwardRef(() => DlDateTimeInputDirective), multi: true}
30+
{provide: NG_VALUE_ACCESSOR, useExisting: DlDateTimeInputDirective, multi: true},
31+
{provide: NG_VALIDATORS, useExisting: DlDateTimeInputDirective, multi: true}
3232
]
3333
})
3434
export class DlDateTimeInputDirective<D> implements ControlValueAccessor, Validator {
3535

3636
/* tslint:disable:member-ordering */
3737
private _filterValidator: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
3838
// @ts-ignore
39-
return (this._inputFilter || ((value: any) => true))(this._value) ?
39+
return (this._inputFilter || (() => true))(this._value) ?
4040
null : {'dlDateTimeInputFilter': {'value': control.value}};
4141
}
4242
private _inputFilter: (value: (D | null)) => boolean = () => true;
@@ -48,7 +48,7 @@ export class DlDateTimeInputDirective<D> implements ControlValueAccessor, Valida
4848
private _changed: ((value: D) => void)[] = [];
4949
private _touched: (() => void)[] = [];
5050
private _validator = Validators.compose([this._parseValidator, this._filterValidator]);
51-
private _validatorOnChange: () => void = () => {};
51+
private _onValidatorChange: () => void = () => {};
5252
private _value: D | undefined = undefined;
5353

5454
/**
@@ -87,8 +87,8 @@ export class DlDateTimeInputDirective<D> implements ControlValueAccessor, Valida
8787
*/
8888
@Input()
8989
set dlDateTimeInputFilter(inputFilterFunction: (value: D | null) => boolean) {
90-
this._inputFilter = inputFilterFunction;
91-
this._validatorOnChange();
90+
this._inputFilter = inputFilterFunction || (() => true);
91+
this._onValidatorChange();
9292
}
9393

9494
/* tslint:enable:member-ordering */
@@ -100,6 +100,19 @@ export class DlDateTimeInputDirective<D> implements ControlValueAccessor, Valida
100100
return this._value;
101101
}
102102

103+
/**
104+
* Set the value of the date/time input to a value of `D` | `undefined` | `null`;
105+
* @param newValue
106+
* the new value of the date/time input
107+
*/
108+
109+
set value(newValue: D | null | undefined) {
110+
if (newValue !== this._value) {
111+
this._value = newValue;
112+
this._changed.forEach(onChanged => onChanged(this._value));
113+
}
114+
}
115+
103116
/**
104117
* Emit a `change` event when the value of the input changes.
105118
*/
@@ -112,7 +125,7 @@ export class DlDateTimeInputDirective<D> implements ControlValueAccessor, Valida
112125
*/
113126
@HostListener('blur') _onBlur() {
114127
if (this._value) {
115-
this.writeValue(this._value);
128+
this._setElementValue(this._value);
116129
}
117130
this._touched.forEach(onTouched => onTouched());
118131
}
@@ -129,8 +142,16 @@ export class DlDateTimeInputDirective<D> implements ControlValueAccessor, Valida
129142
: moment(value, this._inputFormats, true);
130143

131144
this._isValid = testDate && testDate.isValid();
132-
this._value = this._isValid ? this._dateAdapter.fromMilliseconds(testDate.valueOf()) : undefined;
133-
this._changed.forEach(onChanged => onChanged(this._value));
145+
this.value = this._isValid ? this._dateAdapter.fromMilliseconds(testDate.valueOf()) : undefined;
146+
}
147+
148+
/**
149+
* @internal
150+
*/
151+
private _setElementValue(value: D) {
152+
if (value !== null && value !== undefined) {
153+
this._renderer.setProperty(this._elementRef.nativeElement, 'value', moment(value).format(this._displayFormat));
154+
}
134155
}
135156

136157
/**
@@ -151,7 +172,7 @@ export class DlDateTimeInputDirective<D> implements ControlValueAccessor, Valida
151172
* @internal
152173
*/
153174
registerOnValidatorChange(validatorOnChange: () => void): void {
154-
this._validatorOnChange = validatorOnChange;
175+
this._onValidatorChange = validatorOnChange;
155176
}
156177

157178
/**
@@ -172,9 +193,7 @@ export class DlDateTimeInputDirective<D> implements ControlValueAccessor, Valida
172193
* @internal
173194
*/
174195
writeValue(value: D): void {
175-
const normalizedValue = value === null || value === undefined
176-
? ''
177-
: moment(value).format(this._displayFormat);
178-
this._renderer.setProperty(this._elementRef.nativeElement, 'value', normalizedValue);
196+
this.value = value;
197+
this._setElementValue(value);
179198
}
180199
}

src/lib/dl-date-time-input/specs/dl-date-time-input.directive.spec.ts

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {Component, DebugElement, ViewChild} from '@angular/core';
2-
import {async, ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing';
2+
import {async, ComponentFixture, fakeAsync, flush, TestBed, tick} from '@angular/core/testing';
33
import {FormsModule, NgForm} from '@angular/forms';
44
import {By} from '@angular/platform-browser';
55
import * as _moment from 'moment';
@@ -24,7 +24,7 @@ if ('default' in _moment) {
2424
</form>`
2525
})
2626
class DateModelComponent {
27-
dateValue: any;
27+
dateValue: number;
2828
@ViewChild(DlDateTimeInputDirective, {static: false}) input: DlDateTimeInputDirective<number>;
2929
dateTimeFilter: (value: (number | null)) => boolean = () => true;
3030
}
@@ -71,7 +71,7 @@ describe('DlDateTimeInputDirective', () => {
7171
it('should be displayed using default format', fakeAsync(() => {
7272
const octoberFirst = moment('2018-10-01');
7373
const expectedValue = octoberFirst.format(DL_DATE_TIME_DISPLAY_FORMAT_DEFAULT);
74-
component.dateValue = octoberFirst.toDate();
74+
component.dateValue = octoberFirst.valueOf();
7575
fixture.detectChanges();
7676
flush();
7777
const inputElement = debugElement.query(By.directive(DlDateTimeInputDirective)).nativeElement;
@@ -107,7 +107,7 @@ describe('DlDateTimeInputDirective', () => {
107107
expect(inputElement.classList).toContain('ng-touched');
108108
});
109109

110-
it('should reformat the input value on blur', () => {
110+
it('should reformat the input value on blur', fakeAsync(() => {
111111
const inputElement = debugElement.query(By.directive(DlDateTimeInputDirective)).nativeElement;
112112

113113
inputElement.value = '1/1/2001';
@@ -120,19 +120,21 @@ describe('DlDateTimeInputDirective', () => {
120120
fixture.detectChanges();
121121

122122
expect(inputElement.value).toBe(moment('2001-01-01').format(DL_DATE_TIME_DISPLAY_FORMAT_DEFAULT));
123-
});
123+
}));
124124

125125
it('should not reformat invalid dates on blur', () => {
126126
const inputElement = debugElement.query(By.directive(DlDateTimeInputDirective)).nativeElement;
127127

128-
inputElement.value = 'very-valid-date';
128+
inputElement.value = 'very-invalid-date';
129129
inputElement.dispatchEvent(new Event('input'));
130130
fixture.detectChanges();
131131

132+
expect(inputElement.value).toBe('very-invalid-date');
133+
132134
inputElement.dispatchEvent(new Event('blur'));
133135
fixture.detectChanges();
134136

135-
expect(inputElement.value).toBe('very-valid-date');
137+
expect(inputElement.value).toBe('very-invalid-date');
136138
});
137139

138140
it('should consider empty input to be valid (for non-required inputs)', () => {
@@ -143,7 +145,7 @@ describe('DlDateTimeInputDirective', () => {
143145

144146
it('should add ng-invalid on invalid input', fakeAsync(() => {
145147
const novemberFirst = moment('2018-11-01');
146-
component.dateValue = novemberFirst.toDate();
148+
component.dateValue = novemberFirst.valueOf();
147149
fixture.detectChanges();
148150
flush();
149151

@@ -186,10 +188,13 @@ describe('DlDateTimeInputDirective', () => {
186188
expect(inputElement.classList).toContain('ng-valid');
187189
});
188190

189-
it('should add ng-invalid for valid input of filtered date', () => {
190-
const filteredValue = moment('2018-10-29T17:00').valueOf();
191+
it('should add ng-invalid for input of filtered out date', () => {
192+
const expectedErrorValue = moment('2018-10-29T17:00').valueOf();
193+
194+
const allowedValue = moment('2019-10-29T17:00').valueOf();
195+
191196
spyOn(component, 'dateTimeFilter').and.callFake((date: number) => {
192-
return date !== filteredValue;
197+
return date === allowedValue;
193198
});
194199

195200
const inputElement = debugElement.query(By.directive(DlDateTimeInputDirective)).nativeElement;
@@ -201,9 +206,33 @@ describe('DlDateTimeInputDirective', () => {
201206

202207
const control = debugElement.children[0].injector.get(NgForm).control.get('dateValue');
203208
expect(control.hasError('dlDateTimeInputFilter')).toBe(true);
204-
expect(control.errors.dlDateTimeInputFilter.value).toBe(filteredValue.valueOf());
209+
const value = control.errors.dlDateTimeInputFilter.value;
210+
expect(value).toBe(expectedErrorValue);
205211
});
206212

213+
it('should remove ng-invalid when model is updated with valid date', fakeAsync(() => {
214+
const allowedValue = moment('2019-10-29T17:00').valueOf();
215+
spyOn(component, 'dateTimeFilter').and.callFake((date: number) => {
216+
return date === allowedValue;
217+
});
218+
219+
const inputElement = debugElement.query(By.directive(DlDateTimeInputDirective)).nativeElement;
220+
inputElement.value = '10/29/2018 05:00 PM';
221+
inputElement.dispatchEvent(new Event('blur'));
222+
223+
fixture.detectChanges();
224+
225+
expect(inputElement.classList).toContain('ng-invalid');
226+
227+
component.dateValue = allowedValue;
228+
229+
fixture.detectChanges();
230+
tick();
231+
fixture.detectChanges();
232+
233+
expect(inputElement.classList).toContain('ng-valid');
234+
}));
235+
207236
it('should disable input when setDisabled is called', () => {
208237
const inputElement = debugElement.query(By.directive(DlDateTimeInputDirective)).nativeElement;
209238
expect(inputElement.disabled).toBe(false);

0 commit comments

Comments
 (0)