projects/common/lib/components/date/date.component.ts
OnInit
ControlValueAccessor
OnChanges
selector | common-date |
styleUrls | ./date.component.scss |
templateUrl | ./date.component.html |
Properties |
|
Methods |
|
Inputs |
Outputs |
Accessors |
constructor(controlDir: NgControl, injectedValidators: any[])
|
|||||||||
Parameters :
|
date | |
Type : Date
|
|
dateRangeEnd | |
The latest valid date that can be used. Do NOT combine with restrictDates, as they set the same underlying values. |
dateRangeStart | |
The earliest valid date that can be used. Do NOT combine with restrictDates, as they set the same underlying values. |
label | |
Type : string
|
|
Default value : 'Date'
|
|
restrictDate | |
Type : "future" | "past" | "any"
|
|
Default value : 'any'
|
|
Can be one of: "future", "past". "future" includes today, "past" does not. |
disabled | |
Type : boolean
|
|
Default value : false
|
|
Inherited from
AbstractFormControl
|
|
Defined in
AbstractFormControl:16
|
errorMessage | |
Type : ErrorMessage
|
|
Inherited from
AbstractFormControl
|
|
Defined in
AbstractFormControl:19
|
label | |
Type : string
|
|
Inherited from
AbstractFormControl
|
|
Defined in
AbstractFormControl:14
|
dateChange | |
Type : EventEmitter<Date>
|
|
Private _triggerOnChange | ||||||
_triggerOnChange(dt: Date)
|
||||||
Parameters :
Returns :
void
|
Private canCreateDate |
canCreateDate()
|
Returns true if and only if the day/month/year fields are all filled out.
Returns :
boolean
|
Private getNumericValue | ||||||
getNumericValue(value: string)
|
||||||
Convert string to numeric value or null if not
Parameters :
Returns :
number | null
|
ngOnChanges | ||||||
ngOnChanges(changes: SimpleChanges)
|
||||||
Parameters :
Returns :
void
|
ngOnInit |
ngOnInit()
|
Returns :
void
|
onBlurDay | ||||||
onBlurDay(value: string)
|
||||||
Parameters :
Returns :
void
|
onBlurMonth | ||||||
onBlurMonth(value: string)
|
||||||
Parameters :
Returns :
void
|
onBlurYear | ||||||
onBlurYear(value: string)
|
||||||
Parameters :
Returns :
void
|
Private processDate |
processDate()
|
Handles creating / destroying date and emitting changes based on user behaviour.
Returns :
void
|
Private setDisplayVariables |
setDisplayVariables()
|
Returns :
void
|
Private validateDistantDates |
validateDistantDates()
|
Returns :
ValidationErrors | null
|
Private validateRange |
validateRange()
|
Returns :
ValidationErrors | null
|
Private validateSelf |
validateSelf()
|
Validates the DateComponent instance itself, using internal private variables.
Returns :
any
|
writeValue | ||||||
writeValue(value: Date)
|
||||||
Parameters :
Returns :
void
|
ngOnInit |
ngOnInit()
|
Inherited from
AbstractFormControl
|
Defined in
AbstractFormControl:27
|
Returns :
void
|
registerOnChange | ||||||
registerOnChange(fn: any)
|
||||||
Inherited from
AbstractFormControl
|
||||||
Defined in
AbstractFormControl:35
|
||||||
Parameters :
Returns :
void
|
registerOnTouched | ||||||
registerOnTouched(fn: any)
|
||||||
Inherited from
AbstractFormControl
|
||||||
Defined in
AbstractFormControl:40
|
||||||
Parameters :
Returns :
void
|
Protected registerValidation | ||||||||||||
registerValidation(ngControl: NgControl, fn: ValidationErrors)
|
||||||||||||
Inherited from
AbstractFormControl
|
||||||||||||
Defined in
AbstractFormControl:68
|
||||||||||||
Register self validating method
Parameters :
Returns :
any
|
setDisabledState | ||||||
setDisabledState(isDisabled: boolean)
|
||||||
Inherited from
AbstractFormControl
|
||||||
Defined in
AbstractFormControl:45
|
||||||
Parameters :
Returns :
void
|
Protected setErrorMsg |
setErrorMsg()
|
Inherited from
AbstractFormControl
|
Defined in
AbstractFormControl:49
|
Returns :
void
|
Private validateLabel |
validateLabel()
|
Inherited from
AbstractFormControl
|
Defined in
AbstractFormControl:88
|
Returns :
void
|
Abstract writeValue | ||||||
writeValue(value: any)
|
||||||
Inherited from
AbstractFormControl
|
||||||
Defined in
AbstractFormControl:32
|
||||||
Parameters :
Returns :
void
|
_dateRangeEnd |
Type : Date
|
Default value : null
|
_dateRangeStart |
Type : Date
|
Default value : null
|
_day |
Type : string
|
_defaultErrMsg |
Type : ErrorMessage
|
Default value : {
required: `${LabelReplacementTag} is required.`,
dayOutOfRange: `Invalid ${LabelReplacementTag}.`,
yearDistantPast: `Invalid ${LabelReplacementTag}.`,
yearDistantFuture: `Invalid ${LabelReplacementTag}.`,
noPastDatesAllowed: `Invalid ${LabelReplacementTag}.`,
noFutureDatesAllowed: `Invalid ${LabelReplacementTag}.`,
invalidValue: `Invalid ${LabelReplacementTag}.`,
invalidRange: `Invalid ${LabelReplacementTag}.`
}
|
_month |
Type : string
|
Default value : 'null'
|
_year |
Type : string
|
Public controlDir |
Type : NgControl
|
Decorators :
@Optional()
|
dayLabelforId |
Type : string
|
Default value : 'day_' + this.objectId
|
Public isRequired |
Type : boolean
|
monthLabelforId |
Type : string
|
Default value : 'month_' + this.objectId
|
Public monthList |
Type : string[]
|
Default value : [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
]
|
Private today |
Default value : startOfToday()
|
Private tomorrow |
Default value : addDays( this.today, 1 )
|
yearLabelforId |
Type : string
|
Default value : 'year_' + this.objectId
|
Abstract _defaultErrMsg |
Type : ErrorMessage
|
Default value : {}
|
Inherited from
AbstractFormControl
|
Defined in
AbstractFormControl:11
|
_onChange |
Default value : () => {...}
|
Inherited from
AbstractFormControl
|
Defined in
AbstractFormControl:23
|
_onTouched |
Default value : () => {...}
|
Inherited from
AbstractFormControl
|
Defined in
AbstractFormControl:24
|
Public objectId |
Type : string
|
Default value : UUID.UUID()
|
Inherited from
Base
|
Defined in
Base:11
|
An identifier for parents to keep track of components |
dateRangeStart | ||||
setdateRangeStart(dt)
|
||||
The earliest valid date that can be used. Do NOT combine with restrictDates, as they set the same underlying values.
Parameters :
Returns :
void
|
dateRangeEnd | ||||
setdateRangeEnd(dt)
|
||||
The latest valid date that can be used. Do NOT combine with restrictDates, as they set the same underlying values.
Parameters :
Returns :
void
|
month |
getmonth()
|
day |
getday()
|
year |
getyear()
|
Lorem ipsum dolor sit amet consectetur adipisicing elit. Sapiente, magnam ipsam. Sit quasi natus architecto rerum unde non provident! Quia nisi facere amet iste mollitia voluptatem non molestias esse optio?
Aperiam fugiat consectetur temporibus, iste repellat, quisquam sapiente nisi distinctio optio, autem nemo tenetur error eum voluptatibus ab accusamus quis voluptatum blanditiis. Quam et ut reprehenderit vitae nobis, at ipsum!
Exercitationem pariatur animi repudiandae corporis obcaecati ratione ducimus beatae quam, nostrum magnam unde numquam quidem cupiditate odit id. Beatae alias molestiae, optio incidunt harum quia voluptates deserunt sequi. Nesciunt, optio.
import {
Component,
OnInit,
Input,
Output,
EventEmitter,
Optional, Self, SimpleChanges, OnChanges, Inject
} from '@angular/core';
import {
ControlValueAccessor,
NgControl,
ValidationErrors,
NG_VALIDATORS} from '@angular/forms';
import { ErrorMessage, LabelReplacementTag } from '../../models/error-message.interface';
import getDaysInMonth from 'date-fns/getDaysInMonth';
import isAfter from 'date-fns/isAfter';
import isBefore from 'date-fns/isBefore';
import startOfToday from 'date-fns/startOfToday';
import addYears from 'date-fns/addYears';
import subYears from 'date-fns/subYears';
import { MoHCommonLibraryError } from '../../../helpers/library-error';
import { AbstractFormControl } from '../../models/abstract-form-control';
import { compareAsc, startOfDay, addDays } from 'date-fns';
/**
* DateComponent
* You cannot use "[restrictDate]" in combination with either "[dateRangeEnd]" or "[dateRangeStart]".
* You must use either [restrictDate] or the [dateRange*] inputs.
*
* @example
* To trigger 'no future dates allowed' using date ranges set the range end date to yesterday's date.
* <common-date name='effectiveDate'
* label="Effective Date"
* [dateRangeEnd]='yesterday'
* formControlName="effectiveDate"></common-date>
*'
* To trigger 'no past dates allowed' using date ranges set the range start date to today's date.
* <common-date name='effectiveDate'
* label="Effective Date"
* [dateRangeStart]="today"
* formControlName="effectiveDate"></common-date>
*
* To allow instructions under label.
* <common-date name='effectiveDate'
* label="Effective Date"
* [dateRangeEnd]='yesterday'
* formControlName="effectiveDate">
* <p>This is a test.</p>
* </common-date>
* @export
*
*/
const MAX_YEAR_RANGE = 150;
const distantFuture = addYears(startOfToday(), MAX_YEAR_RANGE);
const distantPast = subYears(startOfToday(), MAX_YEAR_RANGE);
@Component({
selector: 'common-date',
templateUrl: './date.component.html',
styleUrls: ['./date.component.scss'],
})
export class DateComponent extends AbstractFormControl
implements OnInit, ControlValueAccessor, OnChanges {
// Inputs for disabled & errorMessage are found in the AbstractFormControl class
@Input() date: Date;
@Output() dateChange: EventEmitter<Date> = new EventEmitter<Date>();
@Input() label: string = 'Date';
/** Can be one of: "future", "past". "future" includes today, "past" does not. */
@Input() restrictDate: 'future' | 'past' | 'any' = 'any';
// The actual values displayed to the user. May not precisely match Date
// object, because these fields can be blank whereas a Date can never have a
// "blank" year for example. All are nullable stings of numbers "0" or "2".
_year: string;
_month: string = 'null'; // this makes it so the blank option is selected in the input
_day: string;
// variables for date ranges
_dateRangeStart: Date = null;
_dateRangeEnd: Date = null;
/**
* The earliest valid date that can be used.
* Do NOT combine with restrictDates, as they set the same underlying values.
*/
@Input()
set dateRangeStart(dt: Date) {
// Set time on date to 00:00:00 for comparing later
this._dateRangeStart = dt ? startOfDay(dt) : null;
}
/**
* The latest valid date that can be used.
* Do NOT combine with restrictDates, as they set the same underlying values.
*/
@Input()
set dateRangeEnd(dt: Date) {
// Set time on date to 00:00:00 for comparing later
this._dateRangeEnd = dt ? startOfDay(dt) : null;
}
public monthList: string[] = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
monthLabelforId: string = 'month_' + this.objectId;
dayLabelforId: string = 'day_' + this.objectId;
yearLabelforId: string = 'year_' + this.objectId;
// Abstact variable defined
_defaultErrMsg: ErrorMessage = {
required: `${LabelReplacementTag} is required.`,
dayOutOfRange: `Invalid ${LabelReplacementTag}.`,
yearDistantPast: `Invalid ${LabelReplacementTag}.`,
yearDistantFuture: `Invalid ${LabelReplacementTag}.`,
noPastDatesAllowed: `Invalid ${LabelReplacementTag}.`,
noFutureDatesAllowed: `Invalid ${LabelReplacementTag}.`,
invalidValue: `Invalid ${LabelReplacementTag}.`,
invalidRange: `Invalid ${LabelReplacementTag}.`
};
private today = startOfToday();
private tomorrow = addDays( this.today, 1 );
public isRequired: boolean; // TODO: remove if not required - value does not get set when using Reactive forms
constructor( @Optional() @Self() public controlDir: NgControl,
@Optional() @Self() @Inject(NG_VALIDATORS) private injectedValidators: any[] ) {
super();
if (controlDir) {
controlDir.valueAccessor = this;
}
}
ngOnChanges(changes: SimpleChanges) {
/*
* Works, creates new object literall
* obj = {
* errorMessage: 'new'message';
* }
*
* Doesn't work, modifies existing object. Would have to manually call cd.detectChanges() afterwards.
* obj.errorMessage = 'newMessage';
*/
if (changes['errorMessage']) {
this.setErrorMsg();
}
}
ngOnInit() {
this.setErrorMsg();
// Set to midnight, so we don't accidentally compare against hours/minutes/seconds
if (this.restrictDate !== 'any' && (this._dateRangeEnd || this._dateRangeStart)) {
const msg = `<common-date> - Invalid @Input() option configuration.
You cannot use "[restrictDate]" in combination with either "[dateRangeEnd]" or "[dateRangeStart]".
You must use either [restrictDate] or the [dateRange*] inputs.
<common-date name='effectiveDate'
label="Effective Date"
[restrictDate]="'past'" <<< problem, choose one
[dateRangeEnd]='today' <<< problem, choose one
formControlName="effectiveDate"
></common-date>
`;
throw new MoHCommonLibraryError(msg);
}
// Initialize date range logic
if (this.restrictDate === 'past') {
// past does allow for today
this._dateRangeEnd = this.today;
this._dateRangeStart = null;
} else if (this.restrictDate === 'future') {
// future does NOT allow for today
this._dateRangeEnd = null;
this._dateRangeStart = this.tomorrow;
}
this.registerValidation( this.controlDir, this.validateSelf )
.then(_ => {
if (this.injectedValidators && this.injectedValidators.length) {
// TODO: Potentially move to AbstractFormControl
// Inspect the validator functions for one that has a {required: true}
// property. Importantly, we are inspecting the validator function
// itself and NOT the current state of the NgControl. -- Does not work for reactive forms
this.isRequired = this.injectedValidators
.filter(x => x.required)
.length >= 1;
}
});
}
get month(): number {
if (this.date) {
return this.date.getMonth();
}
}
get day(): number {
if (this.date) {
return this.date.getDate();
}
}
get year(): number {
if (this.date) {
return this.date.getFullYear();
}
}
/**
* Handles creating / destroying date and emitting changes based on user behaviour.
*/
private processDate() {
if (this.canCreateDate()) {
const year = this.getNumericValue(this._year);
const month = this.getNumericValue(this._month);
const day = this.getNumericValue(this._day);
// Date function appears to use setYear() so any year 0-99 results in year 1900 to 1999
// Set each field individually, use setFullYear() instead of setYear()
// Set time on date to 00:00:00 for comparing later
this.date = startOfDay(new Date(year, month, day));
this.date.setFullYear(year);
} else {
// Trigger validator for emptying fields use case. This is to remove the 'Invalid date' error.
if (this.date ||
(!this._year && !this._day && this._month === 'null')) {
// Destroys the internal Date object.
this.date = null;
}
}
this._onChange(this.date);
this._onTouched(this.date);
this.dateChange.emit(this.date);
}
/**
* Returns true if and only if the day/month/year fields are all filled out.
*/
private canCreateDate(): boolean {
// special because "0" is valid (Jan)
const monthCheck = (typeof this._month === 'string' && this._month !== 'null')
|| typeof this._month === 'number';
if (!!this._year && !!this._day && monthCheck) {
return true;
}
return false;
}
private _triggerOnChange( dt: Date ) {
this._onChange(dt);
this.dateChange.emit(dt);
}
/** Convert string to numeric value or null if not */
private getNumericValue(value: string): number | null {
const parsed = parseInt(value, 10);
return (isNaN(parsed) ? null : parsed);
}
private setDisplayVariables() {
this._day = this.date.getDate().toString();
this._month = this.date.getMonth().toString();
this._year = this.date.getFullYear().toString();
}
writeValue(value: Date): void {
if (value) {
this.date = value;
this.setDisplayVariables();
}
}
onBlurDay(value: string) {
this._day = value;
this.processDate();
}
onBlurYear(value: string) {
this._year = value;
this.processDate();
}
onBlurMonth(value: string) {
this._month = value;
this.processDate();
}
/**
* Validates the DateComponent instance itself, using internal private variables.
*
*/
private validateSelf() {
const year = parseInt(this._year, 10);
const month = parseInt(this._month, 10);
const day = parseInt(this._day, 10);
// Nothing empty fields - nothing to validate OR have required error
if ( isNaN(year) && isNaN(month) && isNaN(day) ) {
return null;
}
// Partially filled out is always invalid, if year is present it must be greater than zero
if ( isNaN(year) || isNaN(month) || isNaN(day) || (!isNaN( year) && year <= 0) ) {
return {invalidValue: true};
}
// We can hardcode the day, since we're only interested in total days for that month.
const daysInMonth = getDaysInMonth(new Date(year, month, 1));
if (day > daysInMonth || day < 1) {
return { dayOutOfRange: true };
}
const dateRangeResult = this.validateRange();
if (dateRangeResult) {
return dateRangeResult;
}
const distantDatesResult = this.validateDistantDates();
if (distantDatesResult) {
return distantDatesResult;
}
return null;
}
// If you set restrictDate, it will return noFutureDatesAllowed / noPastDatesAllowed
// If you just set dateRangeStart / dateRangeEnd, you get invalidRange
private validateRange(): ValidationErrors | null {
const _dt = startOfDay( this.date );
if (this._dateRangeEnd && isAfter(_dt, this._dateRangeEnd)) {
if (this.restrictDate === 'past' ||
compareAsc(this._dateRangeEnd, this.today) === 0) {
return {noFutureDatesAllowed: true};
}
return {invalidRange: true};
}
if (this._dateRangeStart && isBefore(_dt, this._dateRangeStart)) {
if (this.restrictDate === 'future' ||
compareAsc(this._dateRangeStart, this.tomorrow) === 0) {
return {noPastDatesAllowed: true};
}
return {invalidRange: true};
}
return null;
}
private validateDistantDates(): ValidationErrors | null {
// Null end range only allow 150 years in future
if (!this._dateRangeEnd && isAfter(this.date, distantFuture)) {
return {yearDistantFuture: true};
}
// Null start range only allow 150 years in past
if (!this._dateRangeStart && isBefore(this.date, distantPast)) {
return {yearDistantPast: true};
}
return null;
}
}
<fieldset>
<legend class="date--legend">{{label}}</legend>
<ng-container *ngTemplateOutlet="instructionText"></ng-container>
<div class="date-row">
<select class="form-control monthSelect"
id="{{monthLabelforId}}"
name="{{monthLabelforId}}"
[value]="_month"
(blur)="onBlurMonth($event.target.value)"
[disabled]='disabled'
[required]="isRequired"
aria-label="Month">
<!-- We show the blank option so the user can clear out their data.-->
<option value="null" label="Month" selected [disabled]='isRequired'></option>
<option *ngFor="let month of monthList; let i = index;" [value]="i">{{month}}</option>
</select>
<input type="number"
class="form-control dayInput"
id="{{dayLabelforId}}"
name="{{dayLabelforId}}"
placeholder="Day"
[value]="_day"
(blur)="onBlurDay($event.target.value)"
commonDateFieldFormat
[disabled]='disabled'
[required]="isRequired"
maxlength="2"
step="any"
aria-label="Day"
autocomplete="off"/>
<input type="number"
class="form-control yearInput"
id="{{yearLabelforId}}"
name="{{yearLabelforId}}"
placeholder="Year"
[value]="_year"
(blur)="onBlurYear($event.target.value)"
commonDateFieldFormat
[disabled]='disabled'
[required]="isRequired"
maxlength="4"
step="any"
aria-label="Year"
autocomplete="off"/>
</div>
</fieldset>
<common-error-container
[displayError]="!disabled && (controlDir?.touched || controlDir?.dirty) && controlDir.errors">
<div *ngIf="controlDir?.errors?.required; else ComponentErrors">
{{_defaultErrMsg.required}}
</div>
</common-error-container>
<ng-template #instructionText>
<ng-content></ng-content>
</ng-template>
<!--Errors triggered by self-validation within component-->
<ng-template #ComponentErrors>
<div *ngIf="controlDir?.errors?.dayOutOfRange">
{{_defaultErrMsg.dayOutOfRange}}
</div>
<div *ngIf="controlDir?.errors?.yearDistantPast">
{{_defaultErrMsg.yearDistantPast}}
</div>
<div *ngIf="controlDir?.errors?.yearDistantFuture">
{{_defaultErrMsg.yearDistantFuture}}
</div>
<div *ngIf="controlDir?.errors?.noPastDatesAllowed">
{{_defaultErrMsg.noPastDatesAllowed}}
</div>
<div *ngIf="controlDir?.errors?.noFutureDatesAllowed">
{{_defaultErrMsg.noFutureDatesAllowed}}
</div>
<div *ngIf="controlDir?.errors?.invalidRange">
{{_defaultErrMsg.invalidRange}}
</div>
<!-- Case should not happen until something is not formatted correctly-->
<div *ngIf="controlDir?.errors?.invalidValue">
{{_defaultErrMsg.invalidValue}}
</div>
</ng-template>