Skip to content

Commit

Permalink
fix(stark-ui): display the invalid status only when the datepicker is…
Browse files Browse the repository at this point in the history
… touched. Adapt HTML in demo examples

ISSUES CLOSED: #1257
  • Loading branch information
christophercr committed Apr 24, 2019
1 parent e009a2b commit 9a8318d
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 36 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
/* tslint:disable:completed-docs max-inline-declarations no-identical-functions no-big-function */
import { NoopAnimationsModule } from "@angular/platform-browser/animations";
import { By } from "@angular/platform-browser";
import { Component, ViewChild } from "@angular/core";
import { FormControl, FormsModule, ReactiveFormsModule, Validators } from "@angular/forms";
import { async, ComponentFixture, TestBed } from "@angular/core/testing";
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from "@angular/material/core";
import { MatDatepickerModule } from "@angular/material/datepicker";
import { MatFormField, MatFormFieldModule } from "@angular/material/form-field";
import { MatInputModule } from "@angular/material/input";
import { MatMomentDateModule, MomentDateAdapter } from "@angular/material-moment-adapter";
import { TranslateModule } from "@ngx-translate/core";
import { STARK_LOGGING_SERVICE, STARK_ROUTING_SERVICE } from "@nationalbankbelgium/stark-core";
import { MockStarkLoggingService, MockStarkRoutingService } from "@nationalbankbelgium/stark-core/testing";
import { FormControl, FormsModule, ReactiveFormsModule } from "@angular/forms";
import { MatFormFieldModule } from "@angular/material/form-field";
import { DEFAULT_DATE_MASK_CONFIG, StarkDatePickerComponent, StarkDatePickerMaskConfig } from "./date-picker.component";
import { STARK_DATE_FORMATS } from "./date-format.constants";
import { StarkTimestampMaskDirective } from "../../input-mask-directives";
Expand Down Expand Up @@ -106,6 +107,71 @@ describe("DatePickerComponent", () => {
}).compileComponents();
}));

describe("MatFormFieldControl", () => {
let hostComponent: TestHostFormControlComponent;
let hostFixture: ComponentFixture<TestHostFormControlComponent>;
const formFieldInvalidClass = "mat-form-field-invalid";

beforeEach(() => {
hostFixture = TestBed.createComponent(TestHostFormControlComponent);
hostComponent = hostFixture.componentInstance;
hostFixture.detectChanges(); // trigger initial data binding

component = hostComponent.datePickerComponent;
});

it("if date is initially invalid, the date picker should not be displayed as invalid until the user interacts with it", () => {
// re-create component with a form control with "required" validator
hostFixture = TestBed.createComponent(TestHostFormControlComponent);
hostComponent = hostFixture.componentInstance;
hostComponent.formControl = new FormControl(undefined, Validators.required); // initially invalid
hostFixture.detectChanges(); // trigger initial data binding

const formFieldDebugElement = hostFixture.debugElement.query(By.directive(MatFormField));
expect(formFieldDebugElement.classes[formFieldInvalidClass]).toBe(false);

const blurEvent = document.createEvent("Event");
blurEvent.initEvent("blur", true, true);
const inputDebugElement = hostFixture.debugElement.query(By.css("input"));
inputDebugElement.triggerEventHandler("blur", blurEvent); // simulate that the user has touched the input
hostFixture.detectChanges();

expect(formFieldDebugElement.classes[formFieldInvalidClass]).toBe(true);
});

it("if date is initially invalid, the date picker should not be displayed as invalid until it is marked as 'touched'", () => {
// re-create component with a form control with "required" validator
hostFixture = TestBed.createComponent(TestHostFormControlComponent);
hostComponent = hostFixture.componentInstance;
hostComponent.formControl = new FormControl(undefined, Validators.required); // initially invalid
hostFixture.detectChanges(); // trigger initial data binding

const formFieldDebugElement = hostFixture.debugElement.query(By.directive(MatFormField));
expect(formFieldDebugElement.classes[formFieldInvalidClass]).toBe(false);

hostComponent.formControl.markAsTouched();
hostFixture.detectChanges();

expect(formFieldDebugElement.classes[formFieldInvalidClass]).toBe(true);
});

it("if date is initially invalid, the date picker should not be displayed as invalid until it is marked as 'dirty'", () => {
// re-create component with a form control with "required" validator
hostFixture = TestBed.createComponent(TestHostFormControlComponent);
hostComponent = hostFixture.componentInstance;
hostComponent.formControl = new FormControl(undefined, Validators.required); // initially invalid
hostFixture.detectChanges(); // trigger initial data binding

const formFieldDebugElement = hostFixture.debugElement.query(By.directive(MatFormField));
expect(formFieldDebugElement.classes[formFieldInvalidClass]).toBe(false);

hostComponent.formControl.markAsDirty();
hostFixture.detectChanges();

expect(formFieldDebugElement.classes[formFieldInvalidClass]).toBe(true);
});
});

describe("using formControl", () => {
let hostComponent: TestHostFormControlComponent;
let hostFixture: ComponentFixture<TestHostFormControlComponent>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
Expand Down Expand Up @@ -78,15 +79,32 @@ const componentName = "stark-date-picker";
]
})
export class StarkDatePickerComponent extends AbstractStarkUiComponent
implements OnInit, OnChanges, OnDestroy, ControlValueAccessor, Validator, MatFormFieldControl<Date> {
implements OnInit, AfterViewInit, OnChanges, OnDestroy, ControlValueAccessor, Validator, MatFormFieldControl<Date> {
/**
* Part of {@link MatFormFieldControl} API
* @ignore
* @internal
*/
public static nextId = 0;

/**
* Part of {@link MatFormFieldControl} API
* @ignore
*/
@HostBinding()
public id = `stark-date-picker-input-${StarkDatePickerComponent.nextId++}`;

/**
* Part of {@link MatFormFieldControl} API
* @ignore
*/
@HostBinding("attr.aria-describedby")
public describedBy = "";

/**
* Part of {@link MatFormFieldControl} API
* @ignore
*/
@HostBinding("class.floating")
public get shouldLabelFloat(): boolean {
return this.focused || !this.empty;
Expand Down Expand Up @@ -144,8 +162,8 @@ export class StarkDatePickerComponent extends AbstractStarkUiComponent
public min?: Date;

/**
* id attribute of the form field wrapping the mat-datepicker
* id attribute followed by "-input" of the mat-datepicker-input
* The HTML "id" attribute of the date picker's calendar popup.
* This "id" is also used, suffixed with "-input", as the HTML "id" attribute of the date picker's input field.
*/
@Input()
public pickerId = "";
Expand Down Expand Up @@ -229,40 +247,69 @@ export class StarkDatePickerComponent extends AbstractStarkUiComponent
public dateMaskConfig?: StarkTimestampMaskConfig = undefined;

/**
* Part of {@link MatFormFieldControl} API
* @ignore
* @internal
*/
// tslint:disable-next-line:no-null-keyword
public ngControl: NgControl | null = null;

/**
* Variable to define to use MatFormFieldControl.
* Stream that emits whenever the state of the control changes such that the parent `MatFormField`
* needs to run change detection.
* Part of {@link MatFormFieldControl} API
* @ignore
* @internal
*/
public stateChanges: Subject<void> = new Subject<void>();

/**
* Part of {@link MatFormFieldControl} API
* @ignore
* @internal
*/
public focused = false;

/**
* Variable to define to use MatFormFieldControl
* Whether the control is in an error state.
* @ignore
* @internal
*/
public pickerInputTouched = false;

/**
* Part of {@link MatFormFieldControl} API
* @ignore
* @internal
*/
public get errorState(): boolean {
return (
// the control can be in an error state as long as one of these conditions is met:
// 1) the user has interacted with it
// 2) the control is programmatically marked as 'touched' or 'dirty'
const newErrorState =
this.ngControl !== null &&
this.ngControl.control !== null &&
(!!this.ngControl.errors || !!this.pickerInput.validate(this.ngControl.control))
);
(this.pickerInputTouched || !!this.ngControl.touched || !!this.ngControl.dirty) &&
(!!this.ngControl.errors || !!this.pickerInput.validate(this.ngControl.control));

// IMPORTANT: emit a state change when the errorState changes
// This is needed to force the MatFormFieldControl to refresh and render the MatError's
if (this._errorState !== newErrorState) {
this._errorState = newErrorState;
this.stateChanges.next();
}

return this._errorState;
}

/**
* Variable to define to use MatFormFieldControl
* Whether the control is empty.
* The current error state
* @ignore
* @internal
*/
public _errorState = false;

/**
* Part of {@link MatFormFieldControl} API
* @ignore
* @internal
*/
public get empty(): boolean {
return !this.value;
Expand Down Expand Up @@ -334,6 +381,21 @@ export class StarkDatePickerComponent extends AbstractStarkUiComponent
super.ngOnInit();
}

/**
* Component lifecycle hook
*/
public ngAfterViewInit(): void {
const markPickerInputAsTouched = (): void => {
this.pickerInputTouched = true;
this.stateChanges.next();
};

// the picker input should be marked as touched when it has actually been touched or when the calendar is closed
// this way we ensure that the errors are displayed properly when the user interacted with the picker (and not when the picker is pristine)
this.pickerInput.registerOnTouched(markPickerInputAsTouched);
this.picker.closedStream.subscribe(markPickerInputAsTouched);
}

/**
* Component lifecycle hook
*/
Expand Down Expand Up @@ -409,6 +471,8 @@ export class StarkDatePickerComponent extends AbstractStarkUiComponent
}

/**
* @ignore
* @internal
* The registered callback function called when an input event occurs on the input element.
*/
private _onChange: (_: any) => void = (_: any) => {
Expand All @@ -417,41 +481,47 @@ export class StarkDatePickerComponent extends AbstractStarkUiComponent

/**
* @ignore
* @internal
* The registered callback function called when a blur event occurs on the input element.
*/
private _onTouched: () => void = () => {
/*noop*/
};

/**
* @ignore
* @internal
* The registered callback function called when the validator inputs change.
*/
private _onValidatorChange: () => void = () => {
/*noop*/
};

/**
* Registers a function called when the control value changes.
*
* @param fn The callback function
* Part of {@link ControlValueAccessor} API
* Registers a function to be called when the control value changes.
* @ignore
* @internal
*/
public registerOnChange(fn: (_: any) => void): void {
this._onChange = fn;
}

/**
* Registers a function called when the control is touched.
*
* @param fn The callback function
* Part of {@link ControlValueAccessor} API
* Registers a function to be called when the control is touched.
* @ignore
* @internal
*/
public registerOnTouched(fn: () => void): void {
this._onTouched = fn;
}

/**
* Part of {@link ControlValueAccessor} API
* Sets the "disabled" property on the input element.
*
* @param isDisabled The disabled value
* @ignore
* @internal
*/
public setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
Expand All @@ -460,27 +530,28 @@ export class StarkDatePickerComponent extends AbstractStarkUiComponent
}

/**
* Part of {@link ControlValueAccessor} API
* Sets the "value" property on the input element.
*
* @param obj The checked value
* @ignore
* @internal
*/
public writeValue(obj: any): void {
this.value = obj;
}

/**
* Method implemented to use MatFormFieldControl
* Sets the list of element IDs that currently describe this control.
* @param ids - Ids describing the MatFormFieldControl
* Part of {@link MatFormFieldControl} API
* @ignore
* @internal
*/
public setDescribedByIds(ids: string[]): void {
this.describedBy = ids.join(" ");
}

/**
* Method implemented to use MatFormFieldControl
* It handles a click on the control's container.
* @param event - Click Event
* Part of {@link MatFormFieldControl} API
* @ignore
* @internal
*/
public onContainerClick(event: MouseEvent): void {
if ((<Element>event.target).tagName.toLowerCase() !== "input") {
Expand Down Expand Up @@ -557,9 +628,10 @@ export class StarkDatePickerComponent extends AbstractStarkUiComponent
this.value = event.value ? event.value.toDate() : null;

const value: Date | undefined = this.value ? this.value : undefined;
this.dateChange.emit(value);
this._onChange(value);
this._onValidatorChange();
// emit after the model has actually changed
this.dateChange.emit(value);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ <h1 translate>SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST</h1>
pickerId="picker-reactive-form"
pickerName="picker-reactive-form"
placeholder="SHOWCASE.DEMO.DATE_PICKER.PLACEHOLDER"
[dateFilter]="'OnlyWeekends'"
[min]="minDate"
[max]="maxDate"
dateMask
Expand All @@ -21,7 +20,6 @@ <h1 translate>SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST</h1>
></stark-date-picker>
<mat-error>
<div *ngIf="formControl.hasError('required')">This is required</div>
<div *ngIf="formControl.hasError('matDatepickerFilter')">Date filter</div>
<div *ngIf="formControl.hasError('matDatepickerMin')">Min date</div>
<div *ngIf="formControl.hasError('matDatepickerMax')">Max date</div>
</mat-error>
Expand All @@ -46,8 +44,14 @@ <h1 translate>SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST</h1>
[disabled]="disabled"
(dateChange)="onDateNgModelChanged()"
[dateMask]="maskConfig"
#pickerNgModel="ngModel"
required
></stark-date-picker>
<mat-error>
<div *ngIf="pickerNgModel.errors && pickerNgModel.errors['required']">This is required</div>
<div *ngIf="pickerNgModel.errors && pickerNgModel.errors['matDatepickerMin']">Min date</div>
<div *ngIf="pickerNgModel.errors && pickerNgModel.errors['matDatepickerMax']">Max date</div>
</mat-error>
</mat-form-field>
<div>
<mat-checkbox (change)="disabled = !disabled" [value]="disabled">
Expand Down
Loading

0 comments on commit 9a8318d

Please sign in to comment.