import {DatePipe, CurrencyPipe} from '@angular/common';
import {IdlService, IdlObject} from '@eg/core/idl.service';
import {OrgService} from '@eg/core/org.service';
+import {DateUtil} from '@eg/share/util/date';
/**
* Format IDL vield values for display.
return org ? org[orgField]() : '';
case 'timestamp':
- const date = new Date(value);
+ let date;
+
+ if (typeof value === 'string'
+ && value.match(/^\d{4}-\d{2}-\d{2}$/)) {
+ // Date values provided as YYYY-MM-DD with no time
+ // zone (e.g. patron 'dob' field) will be parsed as
+ // UTC dates by Date(...). We want local dates,
+ // though, so let DateUtil handle it.
+ date = DateUtil.ymdToLocalDate(value);
+
+ } else {
+ date = new Date(value);
+ }
+
if (Number.isNaN(date.getTime())) {
console.error('Invalid date in format service', value);
return '';
}
+
let fmt = this.dateFormat || 'shortDate';
if (params.datePlusTime) {
fmt = this.dateTimeFormat || 'short';
}
- let tz;
- if (params.idlField === 'dob') {
- // special case: since dob is the only date column that the
- // IDL thinks of as a timestamp, the date object comes over
- // as a UTC value; apply the correct timezone rather than the
- // local one
- tz = 'UTC';
- }
- return this.datePipe.transform(date, fmt, tz);
+
+ return this.datePipe.transform(date, fmt);
case 'money':
return this.currencyPipe.transform(value);
.eg-date-select {
- max-width: 11em;
-}
-.material-icons {
- font-size: 15px;
+ max-width: 12em;
}
-<ng-container *ngIf="readOnly">
- {{initialDate | formatValue:'timestamp'}}
-</ng-container>
-<ng-container *ngIf="!readOnly">
- <div class="input-group eg-date-select form-validated">
- <input
- class="form-control"
- type="text"
- ngbDatepicker
- #datePicker="ngbDatepicker"
- [attr.id]="domId.length ? domId : null"
- placeholder="yyyy-mm-dd"
- class="form-control"
- container="body"
- name="{{fieldName}}"
- [disabled]="disabled"
- [required]="required"
- (blur)="propagateTouch()"
- [(ngModel)]="current"
- (keyup.enter)="onDateEnter()"
- (dateSelect)="onDateSelect($event)"/>
- <div class="input-group-append">
- <button class="btn btn-outline-secondary" [disabled]="disabled"
- (click)="datePicker.toggle()" type="button">
- <span title="Select Date" i18n-title
- class="material-icons mat-icon-in-button">calendar_today</span>
- </button>
- </div>
- </div>
-</ng-container>
+<div class="eg-date-select form-validated">
+ <input
+ class="form-control"
+ container="body"
+ type="date"
+ [readonly]="readOnly"
+ [attr.id]="domId.length ? domId : null"
+ name="{{fieldName}}"
+ [disabled]="disabled"
+ [required]="required"
+ (blur)="propagateTouch()"
+ [(ngModel)]="current"
+ (change)="onDateSelect($event)"/>
+</div>
import {Component, OnInit, Input, Output, ViewChild, EventEmitter, forwardRef} from '@angular/core';
-import {NgbDateStruct} from '@ng-bootstrap/ng-bootstrap';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
-
-/**
- * RE: displaying locale dates in the input field:
- * https://github.com/ng-bootstrap/ng-bootstrap/issues/754
- * https://stackoverflow.com/questions/40664523/angular2-ngbdatepicker-how-to-format-date-in-inputfield
- */
+import {DateUtil} from '@eg/share/util/date';
@Component({
selector: 'eg-date-select',
@Input() disabled: boolean;
@Input() readOnly: boolean;
- current: NgbDateStruct;
+ current: string; // YYYY-MM-DD
@Output() onChangeAsDate: EventEmitter<Date>;
@Output() onChangeAsIso: EventEmitter<string>;
// convenience methods to access current selected date
currentAsYmd(): string {
- if (this.current == null) { return null; }
- if (!this.isValidDate(this.current)) { return null; }
- return `${this.current.year}-${String(this.current.month).padStart(2, '0')}-${String(this.current.day).padStart(2, '0')}`;
+ return this.current;
}
+
+ // Returns current date with time zone
currentAsIso(): string {
- if (this.current == null) { return null; }
- if (!this.isValidDate(this.current)) { return null; }
- const ymd = `${this.current.year}-${String(this.current.month).padStart(2, '0')}-${String(this.current.day).padStart(2, '0')}`;
- const date = this.localDateFromYmd(ymd);
- const iso = date.toISOString();
- return iso;
+ return DateUtil.ymdToLocalIso(this.current);
}
+
currentAsDate(): Date {
- if (this.current == null) { return null; }
- if (!this.isValidDate(this.current)) { return null; }
- const ymd = `${this.current.year}-${String(this.current.month).padStart(2, '0')}-${String(this.current.day).padStart(2, '0')}`;
- const date = this.localDateFromYmd(ymd);
- return date;
+ return DateUtil.ymdToLocalDate(this.current);
}
// Stub functions required by ControlValueAccessor
ngOnInit() {
- if (this.initialYmd) {
- this.initialDate = this.localDateFromYmd(this.initialYmd);
+ if (this.initialDate) {
+ this.current = DateUtil.dateToLocalYmd(this.initialDate);
} else if (this.initialIso) {
- this.initialDate = new Date(this.initialIso);
- }
+ this.current = DateUtil.isoToLocalYmd(this.initialIso);
- if (this.initialDate) {
- this.writeValue(this.initialDate);
+ } else if (this.initialYmd) {
+ this.current = this.initialYmd;
}
}
- isValidDate(dt: NgbDateStruct): dt is NgbDateStruct {
- return (<NgbDateStruct>dt).year !== undefined;
- }
-
- onDateEnter() {
- if (this.current === null) {
+ onDateSelect(evt) {
+ if (this.current === '' || this.current === null) {
this.onCleared.emit('cleared');
- } else if (this.isValidDate(this.current)) {
- this.onDateSelect(this.current);
+ return;
}
- // ignoring invalid input for now
- }
- onDateSelect(evt) {
- const ymd = `${evt.year}-${String(evt.month).padStart(2, '0')}-${String(evt.day).padStart(2, '0')}`;
- const date = this.localDateFromYmd(ymd);
+ const date = DateUtil.ymdToLocalDate(this.current);
const iso = date.toISOString();
+
this.onChangeAsDate.emit(date);
- this.onChangeAsYmd.emit(ymd);
+ this.onChangeAsYmd.emit(this.current);
this.onChangeAsIso.emit(iso);
this.propagateChange(date);
}
- // Create a date in the local time zone with selected YMD values.
- // TODO: Consider moving this to a date service...
- localDateFromYmd(ymd: string): Date {
- const parts = ymd.split('-');
- return new Date(
- Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
- }
-
reset() {
- this.current = {
- year: null,
- month: null,
- day: null
- };
+ this.current = ''; // browser uses "" when clearing the value
}
writeValue(value: Date) {
if (value) {
- this.current = {
- year: value.getFullYear(),
- month: value.getMonth() + 1,
- day: value.getDate()
- };
+ this.current = DateUtil.dateToLocalYmd(value);
}
}
--- /dev/null
+import {DateUtil} from './date';
+
+// PhantomJS does not yet support String.padStart()
+// Add polyfil for testing.
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart#Polyfill
+if (!String.prototype.padStart) {
+ String.prototype.padStart = function padStart(targetLength, padString) {
+ padString = String(typeof padString !== 'undefined' ? padString : ' ');
+ if (this.length >= targetLength) {
+ return String(this);
+ } else {
+ targetLength = targetLength - this.length;
+ if (targetLength > padString.length) {
+ // append to original to ensure we are longer than needed
+ padString += padString.repeat(targetLength / padString.length);
+ }
+ return padString.slice(0, targetLength) + String(this);
+ }
+ };
+}
+
+describe('DateUtil', () => {
+
+ // date and parts in the local time zone
+ const now = new Date();
+ const year = now.getFullYear();
+ const day = String(now.getDate()).padStart(2, '0');
+ const mon = String(now.getMonth() + 1).padStart(2, '0');
+ const ymd = '2020-02-29'; // leap year
+
+ it('Create YMD from date', () => {
+ expect(DateUtil.dateToLocalYmd(now)).toBe(`${year}-${mon}-${day}`);
+ });
+
+ it('Cross walk YMD and ISO', () => {
+ expect(DateUtil.isoToLocalYmd(DateUtil.ymdToLocalIso(ymd))).toBe(ymd);
+ });
+});
--- /dev/null
+/* Collection of date utility functions */
+
+export class DateUtil {
+
+ // Returns a YYYY-MM-DD string in the local time zone matching
+ // the provided Date object.
+ static dateToLocalYmd(d: Date): string {
+ if (!d) { return null; }
+
+ const day = String(d.getDate()).padStart(2, '0');
+ const mon = String(d.getMonth() + 1).padStart(2, '0');
+
+ return `${d.getFullYear()}-${mon}-${day}`;
+ }
+
+ // Returns a YYYY-MM-DD string in the local time zone matching
+ // the provided ISO string.
+ static isoToLocalYmd(iso: string): string {
+ return DateUtil.dateToLocalYmd(new Date(iso));
+ }
+
+ // Return a date object in the local time zone matching the
+ // provided YYYY-MM-DD
+ static ymdToLocalDate(ymd: string): Date {
+ if (!ymd) { return null; }
+
+ // Date(YYYY-MM-DD) => UTC date.
+ // Date(YYYY, MM, DD) => local date
+
+ const parts = ymd.split('-');
+ return new Date(
+ Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]));
+ }
+
+ // Return an ISO string in the local time zone matching the
+ // provided YYYY-MM-DD
+ static ymdToLocalIso(ymd: string): string {
+ const d = DateUtil.ymdToLocalDate(ymd);
+ if (!d) { return null; }
+ return d.toISOString();
+ }
+}
<button class="btn btn-success" (click)="testStrings()">Test Strings</button>
</div>
-<div class="row">
- <div class="form-group">
- <eg-date-select (onChangeAsDate)="changeDate($event)"
- initialYmd="2017-03-04">
+<div class="row mb-3">
+ <div class="col-lg-3"><!-- verify max-width css -->
+ <eg-date-select (onChangeAsDate)="changeDate($event)"
+ (onCleared)="testDate = null" [initialDate]="testDate">
</eg-date-select>
</div>
- <div>HERE: {{testDate}}</div>
+ <div class="col-lg-9">
+ <div *ngIf="testDate">Date Set To: {{testDate}}</div>
+ <div *ngIf="!testDate">Date Cleared</div>
+ </div>
</div>
<!-- printing -->
renderLocal = false;
- testDate: any;
+ testDate: Date = new Date();
testStr: string;
@Input() set testString(str: string) {
}
changeDate(date) {
- console.log('HERE WITH ' + date);
this.testDate = date;
}