From 5473cd383d654ec34f675877c9aa113e6673f819 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Mon, 19 Aug 2019 12:06:40 -0400 Subject: [PATCH] LP1840782 Angular native date select & DateUtil class Migrate implemenation from ng-bootstrap to the browser-native element. Create a DateUtil class to package some common date transforms, etc. 'DOB' handling in FormatService leverages the new DateUtil class for generating a local date from Y/M/D and does so any time a simple Y/M/D value is passed, instead of limiting to 'DOB' values. Sandbox example expanded to demonstrate date clearing and improve layout. Signed-off-by: Bill Erickson --- Open-ILS/src/eg2/src/app/core/format.service.ts | 28 +++++--- .../share/date-select/date-select.component.css | 5 +- .../share/date-select/date-select.component.html | 44 ++++-------- .../app/share/date-select/date-select.component.ts | 81 ++++++---------------- Open-ILS/src/eg2/src/app/share/util/date.spec.ts | 38 ++++++++++ Open-ILS/src/eg2/src/app/share/util/date.ts | 42 +++++++++++ .../src/app/staff/sandbox/sandbox.component.html | 13 ++-- .../eg2/src/app/staff/sandbox/sandbox.component.ts | 3 +- 8 files changed, 143 insertions(+), 111 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/share/util/date.spec.ts create mode 100644 Open-ILS/src/eg2/src/app/share/util/date.ts diff --git a/Open-ILS/src/eg2/src/app/core/format.service.ts b/Open-ILS/src/eg2/src/app/core/format.service.ts index 63aeec66f0..06a6e19841 100644 --- a/Open-ILS/src/eg2/src/app/core/format.service.ts +++ b/Open-ILS/src/eg2/src/app/core/format.service.ts @@ -2,6 +2,7 @@ import {Injectable, Pipe, PipeTransform} from '@angular/core'; 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. @@ -107,24 +108,31 @@ export class FormatService { 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) are assumed by Date(...) to + // be UTC dates. Ask DateUtil to give us a date in the + // current locale instead. + 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); diff --git a/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.css b/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.css index e4ab7a0438..3b3a49b4b9 100644 --- a/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.css +++ b/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.css @@ -1,6 +1,3 @@ .eg-date-select { - max-width: 11em; -} -.material-icons { - font-size: 15px; + max-width: 12em; } diff --git a/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html b/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html index 80f0188482..9401baa432 100644 --- a/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html +++ b/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.html @@ -1,30 +1,14 @@ - - {{initialDate | formatValue:'timestamp'}} - - -
- -
- -
-
-
+
+ +
diff --git a/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.ts b/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.ts index 66d363af15..811bfcf466 100644 --- a/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.ts +++ b/Open-ILS/src/eg2/src/app/share/date-select/date-select.component.ts @@ -1,12 +1,6 @@ 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', @@ -29,7 +23,7 @@ export class DateSelectComponent implements OnInit, ControlValueAccessor { @Input() disabled: boolean; @Input() readOnly: boolean; - current: NgbDateStruct; + current: string; // YYYY-MM-DD @Output() onChangeAsDate: EventEmitter; @Output() onChangeAsIso: EventEmitter; @@ -38,24 +32,16 @@ export class DateSelectComponent implements OnInit, ControlValueAccessor { // 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 @@ -71,64 +57,39 @@ export class DateSelectComponent implements OnInit, 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 (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); } } diff --git a/Open-ILS/src/eg2/src/app/share/util/date.spec.ts b/Open-ILS/src/eg2/src/app/share/util/date.spec.ts new file mode 100644 index 0000000000..2ef5a42b8a --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/util/date.spec.ts @@ -0,0 +1,38 @@ +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); + }); +}); diff --git a/Open-ILS/src/eg2/src/app/share/util/date.ts b/Open-ILS/src/eg2/src/app/share/util/date.ts new file mode 100644 index 0000000000..6f2516fb5e --- /dev/null +++ b/Open-ILS/src/eg2/src/app/share/util/date.ts @@ -0,0 +1,42 @@ +/* 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(); + } +} diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html index 2febd8eb3b..adb01d3ec3 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html @@ -131,13 +131,16 @@ -
-
- +
+
+
-
HERE: {{testDate}}
+
+
Date Set To: {{testDate}}
+
Date Cleared
+
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts index 740d4d14b8..c452e158ab 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts @@ -66,7 +66,7 @@ export class SandboxComponent implements OnInit { renderLocal = false; - testDate: any; + testDate: Date = new Date(); testStr: string; @Input() set testString(str: string) { @@ -324,7 +324,6 @@ export class SandboxComponent implements OnInit { } changeDate(date) { - console.log('HERE WITH ' + date); this.testDate = date; } -- 2.11.0