Skip to content

Commit

Permalink
fix(input-directive): input does not remove ng-invalid with valid mod…
Browse files Browse the repository at this point in the history
…el 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
  • Loading branch information
dalelotts committed Nov 17, 2019
1 parent ceb404a commit 6193c38
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 33 deletions.
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

49 changes: 34 additions & 15 deletions src/lib/dl-date-time-input/dl-date-time-input.directive.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Directive, ElementRef, EventEmitter, forwardRef, HostListener, Inject, Input, Output, Renderer2} from '@angular/core';
import {Directive, ElementRef, EventEmitter, HostListener, Inject, Input, Output, Renderer2} from '@angular/core';
import {
AbstractControl,
ControlValueAccessor,
Expand Down Expand Up @@ -27,16 +27,16 @@ const moment = _moment;
@Directive({
selector: 'input[dlDateTimeInput]',
providers: [
{provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => DlDateTimeInputDirective), multi: true},
{provide: NG_VALIDATORS, useExisting: forwardRef(() => DlDateTimeInputDirective), multi: true}
{provide: NG_VALUE_ACCESSOR, useExisting: DlDateTimeInputDirective, multi: true},
{provide: NG_VALIDATORS, useExisting: DlDateTimeInputDirective, multi: true}
]
})
export class DlDateTimeInputDirective<D> implements ControlValueAccessor, Validator {

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

/**
Expand Down Expand Up @@ -87,8 +87,8 @@ export class DlDateTimeInputDirective<D> implements ControlValueAccessor, Valida
*/
@Input()
set dlDateTimeInputFilter(inputFilterFunction: (value: D | null) => boolean) {
this._inputFilter = inputFilterFunction;
this._validatorOnChange();
this._inputFilter = inputFilterFunction || (() => true);
this._onValidatorChange();
}

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

/**
* Set the value of the date/time input to a value of `D` | `undefined` | `null`;
* @param newValue
* the new value of the date/time input
*/

set value(newValue: D | null | undefined) {
if (newValue !== this._value) {
this._value = newValue;
this._changed.forEach(onChanged => onChanged(this._value));
}
}

/**
* Emit a `change` event when the value of the input changes.
*/
Expand All @@ -112,7 +125,7 @@ export class DlDateTimeInputDirective<D> implements ControlValueAccessor, Valida
*/
@HostListener('blur') _onBlur() {
if (this._value) {
this.writeValue(this._value);
this._setElementValue(this._value);
}
this._touched.forEach(onTouched => onTouched());
}
Expand All @@ -129,8 +142,16 @@ export class DlDateTimeInputDirective<D> implements ControlValueAccessor, Valida
: moment(value, this._inputFormats, true);

this._isValid = testDate && testDate.isValid();
this._value = this._isValid ? this._dateAdapter.fromMilliseconds(testDate.valueOf()) : undefined;
this._changed.forEach(onChanged => onChanged(this._value));
this.value = this._isValid ? this._dateAdapter.fromMilliseconds(testDate.valueOf()) : undefined;
}

/**
* @internal
*/
private _setElementValue(value: D) {
if (value !== null && value !== undefined) {
this._renderer.setProperty(this._elementRef.nativeElement, 'value', moment(value).format(this._displayFormat));
}
}

/**
Expand All @@ -151,7 +172,7 @@ export class DlDateTimeInputDirective<D> implements ControlValueAccessor, Valida
* @internal
*/
registerOnValidatorChange(validatorOnChange: () => void): void {
this._validatorOnChange = validatorOnChange;
this._onValidatorChange = validatorOnChange;
}

/**
Expand All @@ -172,9 +193,7 @@ export class DlDateTimeInputDirective<D> implements ControlValueAccessor, Valida
* @internal
*/
writeValue(value: D): void {
const normalizedValue = value === null || value === undefined
? ''
: moment(value).format(this._displayFormat);
this._renderer.setProperty(this._elementRef.nativeElement, 'value', normalizedValue);
this.value = value;
this._setElementValue(value);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Component, DebugElement, ViewChild} from '@angular/core';
import {async, ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing';
import {async, ComponentFixture, fakeAsync, flush, TestBed, tick} from '@angular/core/testing';
import {FormsModule, NgForm} from '@angular/forms';
import {By} from '@angular/platform-browser';
import * as _moment from 'moment';
Expand All @@ -24,7 +24,7 @@ if ('default' in _moment) {
</form>`
})
class DateModelComponent {
dateValue: any;
dateValue: number;
@ViewChild(DlDateTimeInputDirective, {static: false}) input: DlDateTimeInputDirective<number>;
dateTimeFilter: (value: (number | null)) => boolean = () => true;
}
Expand Down Expand Up @@ -71,7 +71,7 @@ describe('DlDateTimeInputDirective', () => {
it('should be displayed using default format', fakeAsync(() => {
const octoberFirst = moment('2018-10-01');
const expectedValue = octoberFirst.format(DL_DATE_TIME_DISPLAY_FORMAT_DEFAULT);
component.dateValue = octoberFirst.toDate();
component.dateValue = octoberFirst.valueOf();
fixture.detectChanges();
flush();
const inputElement = debugElement.query(By.directive(DlDateTimeInputDirective)).nativeElement;
Expand Down Expand Up @@ -107,7 +107,7 @@ describe('DlDateTimeInputDirective', () => {
expect(inputElement.classList).toContain('ng-touched');
});

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

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

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

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

inputElement.value = 'very-valid-date';
inputElement.value = 'very-invalid-date';
inputElement.dispatchEvent(new Event('input'));
fixture.detectChanges();

expect(inputElement.value).toBe('very-invalid-date');

inputElement.dispatchEvent(new Event('blur'));
fixture.detectChanges();

expect(inputElement.value).toBe('very-valid-date');
expect(inputElement.value).toBe('very-invalid-date');
});

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

it('should add ng-invalid on invalid input', fakeAsync(() => {
const novemberFirst = moment('2018-11-01');
component.dateValue = novemberFirst.toDate();
component.dateValue = novemberFirst.valueOf();
fixture.detectChanges();
flush();

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

it('should add ng-invalid for valid input of filtered date', () => {
const filteredValue = moment('2018-10-29T17:00').valueOf();
it('should add ng-invalid for input of filtered out date', () => {
const expectedErrorValue = moment('2018-10-29T17:00').valueOf();

const allowedValue = moment('2019-10-29T17:00').valueOf();

spyOn(component, 'dateTimeFilter').and.callFake((date: number) => {
return date !== filteredValue;
return date === allowedValue;
});

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

const control = debugElement.children[0].injector.get(NgForm).control.get('dateValue');
expect(control.hasError('dlDateTimeInputFilter')).toBe(true);
expect(control.errors.dlDateTimeInputFilter.value).toBe(filteredValue.valueOf());
const value = control.errors.dlDateTimeInputFilter.value;
expect(value).toBe(expectedErrorValue);
});

it('should remove ng-invalid when model is updated with valid date', fakeAsync(() => {
const allowedValue = moment('2019-10-29T17:00').valueOf();
spyOn(component, 'dateTimeFilter').and.callFake((date: number) => {
return date === allowedValue;
});

const inputElement = debugElement.query(By.directive(DlDateTimeInputDirective)).nativeElement;
inputElement.value = '10/29/2018 05:00 PM';
inputElement.dispatchEvent(new Event('blur'));

fixture.detectChanges();

expect(inputElement.classList).toContain('ng-invalid');

component.dateValue = allowedValue;

fixture.detectChanges();
tick();
fixture.detectChanges();

expect(inputElement.classList).toContain('ng-valid');
}));

it('should disable input when setDisabled is called', () => {
const inputElement = debugElement.query(By.directive(DlDateTimeInputDirective)).nativeElement;
expect(inputElement.disabled).toBe(false);
Expand Down

0 comments on commit 6193c38

Please sign in to comment.