File

projects/common/lib/components/date/date.component.ts

Extends

AbstractFormControl

Implements

OnInit ControlValueAccessor OnChanges

Metadata

selector common-date
styleUrls ./date.component.scss
templateUrl ./date.component.html

Index

Properties
Methods
Inputs
Outputs
Accessors

Constructor

constructor(controlDir: NgControl, injectedValidators: any[])
Parameters :
Name Type Optional
controlDir NgControl No
injectedValidators any[] No

Inputs

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
errorMessage
Type : ErrorMessage
Inherited from AbstractFormControl
label
Type : string
Inherited from AbstractFormControl

Outputs

dateChange
Type : EventEmitter<Date>

Methods

Private _triggerOnChange
_triggerOnChange(dt: Date)
Parameters :
Name Type Optional
dt Date No
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 :
Name Type Optional
value string No
Returns : number | null
ngOnChanges
ngOnChanges(changes: SimpleChanges)
Parameters :
Name Type Optional
changes SimpleChanges No
Returns : void
ngOnInit
ngOnInit()
Returns : void
onBlurDay
onBlurDay(value: string)
Parameters :
Name Type Optional
value string No
Returns : void
onBlurMonth
onBlurMonth(value: string)
Parameters :
Name Type Optional
value string No
Returns : void
onBlurYear
onBlurYear(value: string)
Parameters :
Name Type Optional
value string No
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 :
Name Type Optional
value Date No
Returns : void
ngOnInit
ngOnInit()
Inherited from AbstractFormControl
Returns : void
registerOnChange
registerOnChange(fn: any)
Inherited from AbstractFormControl
Parameters :
Name Type Optional
fn any No
Returns : void
registerOnTouched
registerOnTouched(fn: any)
Inherited from AbstractFormControl
Parameters :
Name Type Optional
fn any No
Returns : void
Protected registerValidation
registerValidation(ngControl: NgControl, fn: ValidationErrors)
Inherited from AbstractFormControl

Register self validating method

Parameters :
Name Type Optional Description
ngControl NgControl No
fn ValidationErrors No

function for validating self

Returns : any
setDisabledState
setDisabledState(isDisabled: boolean)
Inherited from AbstractFormControl
Parameters :
Name Type Optional
isDisabled boolean No
Returns : void
Protected setErrorMsg
setErrorMsg()
Inherited from AbstractFormControl
Returns : void
Private validateLabel
validateLabel()
Inherited from AbstractFormControl
Returns : void
Abstract writeValue
writeValue(value: any)
Inherited from AbstractFormControl
Parameters :
Name Type Optional
value any No
Returns : void

Properties

_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()
@Self()
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
_onChange
Default value : () => {...}
Inherited from AbstractFormControl
_onTouched
Default value : () => {...}
Inherited from AbstractFormControl
Public objectId
Type : string
Default value : UUID.UUID()
Inherited from Base
Defined in Base:11

An identifier for parents to keep track of components

Accessors

dateRangeStart
setdateRangeStart(dt)

The earliest valid date that can be used. Do NOT combine with restrictDates, as they set the same underlying values.

Parameters :
Name Optional
dt No
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 :
Name Optional
dt No
Returns : void
month
getmonth()
day
getday()
year
getyear()

Design Guidelines

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>
Legend
Html element
Component
Html element with directive

result-matching ""

    No results matching ""