From 4faf8434c6173ba1a36334aeeeec5a83b2990405 Mon Sep 17 00:00:00 2001 From: Lukas Boll Date: Mon, 12 Feb 2024 11:24:27 +0100 Subject: [PATCH] feat(angular-material): Date Picker Format and View Use a custom dateFormatter to enable the date picker format option. Enable the JSON Forms view option. --- packages/angular-material/package.json | 2 + .../src/library/controls/date.renderer.ts | 115 ++++++++++++++++-- .../angular-material/src/library/module.ts | 29 +---- .../src/library/util/date-format.ts | 25 ++++ .../{ => library}/util/dayjs-date-adapter.ts | 58 ++++++--- .../test/date-control.spec.ts | 16 ++- packages/examples/src/examples/dates.ts | 2 +- pnpm-lock.yaml | 10 ++ 8 files changed, 197 insertions(+), 60 deletions(-) create mode 100644 packages/angular-material/src/library/util/date-format.ts rename packages/angular-material/src/{ => library}/util/dayjs-date-adapter.ts (60%) diff --git a/packages/angular-material/package.json b/packages/angular-material/package.json index 02433de6c2..b66f955afc 100644 --- a/packages/angular-material/package.json +++ b/packages/angular-material/package.json @@ -70,6 +70,7 @@ "@angular/router": "^16.0.0 || ^17.0.0", "@jsonforms/angular": "3.2.1", "@jsonforms/core": "3.2.1", + "dayjs": "^1.11.7", "rxjs": "^6.6.0 || ^7.4.0" }, "dependencies": { @@ -108,6 +109,7 @@ "@typescript-eslint/parser": "^5.54.1", "babel-loader": "^8.0.6", "copy-webpack-plugin": "^11.0.0", + "dayjs": "^1.11.10", "eslint": "^7.32.0", "eslint-config-prettier": "^8.7.0", "eslint-plugin-import": "^2.27.5", diff --git a/packages/angular-material/src/library/controls/date.renderer.ts b/packages/angular-material/src/library/controls/date.renderer.ts index 3cc26c1c2c..20bcd361bd 100644 --- a/packages/angular-material/src/library/controls/date.renderer.ts +++ b/packages/angular-material/src/library/controls/date.renderer.ts @@ -22,15 +22,33 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { Component, ChangeDetectionStrategy } from '@angular/core'; -import { isDateControl, RankedTester, rankWith } from '@jsonforms/core'; +import { + Component, + ChangeDetectionStrategy, + Inject, + ViewEncapsulation, +} from '@angular/core'; +import { + defaultDateFormat, + isDateControl, + JsonFormsState, + RankedTester, + rankWith, + StatePropsOfControl, +} from '@jsonforms/core'; import { JsonFormsAngularService, JsonFormsControl } from '@jsonforms/angular'; -import { DateAdapter } from '@angular/material/core'; +import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core'; +import { MyFormat } from '../util/date-format'; +import { DayJsDateAdapter } from '../util/dayjs-date-adapter'; +import { MatDatepicker } from '@angular/material/datepicker'; @Component({ selector: 'DateControlRenderer', template: ` - + {{ label }} - + {{ description }} @@ -54,28 +76,99 @@ import { DateAdapter } from '@angular/material/core'; `, styles: [ ` - :host { + DateControlRenderer { display: flex; flex-direction: row; } - mat-form-field { + .date-control-renderer { flex: 1 1 auto; } + .no-panel-navigation .mat-calendar-period-button { + pointer-events: none; + } + .no-panel-navigation .mat-calendar-arrow { + display: none; + } `, ], + encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, + providers: [ + { + provide: DateAdapter, + useClass: DayJsDateAdapter, + }, + { + provide: MAT_DATE_FORMATS, + useClass: MyFormat, + }, + ], }) export class DateControlRenderer extends JsonFormsControl { + views: string[] = []; + startView = ''; + panelClass = ''; + constructor( jsonformsService: JsonFormsAngularService, - private dateAdapter: DateAdapter + @Inject(MAT_DATE_FORMATS) public dateFormat: MyFormat, + @Inject(DateAdapter) public dateAdapter: DayJsDateAdapter ) { super(jsonformsService); } getEventValue = (event: any) => { - return this.dateAdapter.format(event.value, 'YYYY-MM-DD'); + const value = event.value ? event.value : event; + return this.dateAdapter.toSaveFormat(value); }; + + protected mapToProps(state: JsonFormsState): StatePropsOfControl { + const props = super.mapToProps(state); + const saveFormat = this.uischema?.options?.dateSaveFormat + ? this.uischema.options.dateSaveFormat + : defaultDateFormat; + this.views = this.uischema?.options?.views + ? this.uischema.options.views + : ['year', 'month', 'day']; + this.setViewProperties(); + + const dateFormat = this.uischema?.options?.dateFormat; + + if (this.uischema?.options?.dateFormat) { + this.dateAdapter.setDisplayFormat(dateFormat); + this.dateFormat.setDisplayFormat(dateFormat); + } + + this.dateAdapter.setSaveFormat(saveFormat); + this.dateFormat.setSaveFormat(saveFormat); + const date = this.dateAdapter.parse(props.data); + return { ...props, data: date }; + } + + yearSelected($event: any, datepicker: MatDatepicker) { + if (!this.views.includes('day') && !this.views.includes('month')) { + this.onChange($event); + datepicker.close(); + } + } + monthSelected($event: any, datepicker: MatDatepicker) { + if (!this.views.includes('day')) { + this.onChange($event); + datepicker.close(); + } + } + + setViewProperties() { + if (!this.views.includes('day') && !this.views.includes('month')) { + this.startView = 'multi-year'; + this.panelClass = 'no-panel-navigation'; + } else if (!this.views.includes('day')) { + this.startView = 'multi-year'; + this.panelClass = 'no-panel-navigation'; + } else { + this.startView = 'month'; + } + } } export const DateControlRendererTester: RankedTester = rankWith( diff --git a/packages/angular-material/src/library/module.ts b/packages/angular-material/src/library/module.ts index 95f7558b9b..f3c11064bd 100644 --- a/packages/angular-material/src/library/module.ts +++ b/packages/angular-material/src/library/module.ts @@ -22,7 +22,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -import { CommonModule, DatePipe } from '@angular/common'; +import { CommonModule } from '@angular/common'; import { ReactiveFormsModule } from '@angular/forms'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; @@ -42,11 +42,7 @@ import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatTableModule } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; import { MatToolbarModule } from '@angular/material/toolbar'; -import { - DateAdapter, - MatNativeDateModule, - MAT_DATE_FORMATS, -} from '@angular/material/core'; +import { MatNativeDateModule } from '@angular/material/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { JsonFormsModule } from '@jsonforms/angular'; import { AutocompleteControlRenderer } from './controls/autocomplete.renderer'; @@ -68,7 +64,6 @@ import { HorizontalLayoutRenderer } from './layouts/horizontal-layout.renderer'; import { VerticalLayoutRenderer } from './layouts/vertical-layout.renderer'; import { ArrayLayoutRenderer } from './layouts/array-layout.renderer'; import { LayoutChildrenRenderPropsPipe } from './layouts'; -import { DayJsDateAdapter } from './util/dayjs-date-adapter'; @NgModule({ imports: [ @@ -137,25 +132,5 @@ import { DayJsDateAdapter } from './util/dayjs-date-adapter'; MatAutocompleteModule, ], schemas: [CUSTOM_ELEMENTS_SCHEMA], - providers: [ - { - provide: DateAdapter, - useClass: DayJsDateAdapter, - }, - { - provide: MAT_DATE_FORMATS, - useValue: { - parse: { - dateInput: 'YYYY-MM-DD', - }, - display: { - dateInput: 'YYYY-MM-DD', - monthYearLabel: 'YYYY-MM', - dateA11yLabel: 'YYYY-MM-DD', - monthYearA11yLabel: 'YYYY-MM', - }, - }, - }, - ], }) export class JsonFormsAngularMaterialModule {} diff --git a/packages/angular-material/src/library/util/date-format.ts b/packages/angular-material/src/library/util/date-format.ts new file mode 100644 index 0000000000..de31c1e0c9 --- /dev/null +++ b/packages/angular-material/src/library/util/date-format.ts @@ -0,0 +1,25 @@ +import { defaultDateFormat } from '@jsonforms/core'; + +export class MyFormat { + saveFormat: string = defaultDateFormat; + displayFormat = 'M/D/YYYY'; + + setSaveFormat(saveFormat: string) { + this.saveFormat = saveFormat; + } + + setDisplayFormat(displayFormat: string) { + this.saveFormat = displayFormat; + } + + get display() { + return { + dateInput: this.displayFormat, + }; + } + get parse() { + return { + dateInput: this.saveFormat, + }; + } +} diff --git a/packages/angular-material/src/util/dayjs-date-adapter.ts b/packages/angular-material/src/library/util/dayjs-date-adapter.ts similarity index 60% rename from packages/angular-material/src/util/dayjs-date-adapter.ts rename to packages/angular-material/src/library/util/dayjs-date-adapter.ts index aa70f14027..ef1bec70e5 100644 --- a/packages/angular-material/src/util/dayjs-date-adapter.ts +++ b/packages/angular-material/src/library/util/dayjs-date-adapter.ts @@ -23,7 +23,9 @@ THE SOFTWARE. */ +import { Injectable } from '@angular/core'; import { NativeDateAdapter } from '@angular/material/core'; +import { defaultDateFormat } from '@jsonforms/core'; import dayjs from 'dayjs'; import customParsing from 'dayjs/plugin/customParseFormat'; @@ -31,19 +33,32 @@ import customParsing from 'dayjs/plugin/customParseFormat'; dayjs.extend(customParsing); /** - * date adapter for dayjs to parse and format dates + * date adapter for dayjs to parse and format dates */ +@Injectable() export class DayJsDateAdapter extends NativeDateAdapter { + saveFormat: string = defaultDateFormat; + displayFormat = 'M/D/YYYY'; + + setSaveFormat(format: string) { + this.saveFormat = format; + } + + setDisplayFormat(displayFormat: string) { + this.displayFormat = displayFormat; + } + /** - * parses a given user input string in the YYYY-MM-DD format into a date object - * @param value date string to be parsed (YYYY-MM-DD) - * @returns date object or null if parsing failed - */ - parse(value: any): Date | null { + * parses a given data prop string in the save-format into a date object + * @param value date string to be parsed + * @returns date object or null if parsing failed + */ + parse(value: string): Date | null { if (!value) { return null; - } - const date = dayjs(value, 'YYYY-MM-DD', true); + } + const date = dayjs(value, this.saveFormat); + if (date.isValid()) { return date.toDate(); } else { @@ -51,14 +66,26 @@ export class DayJsDateAdapter extends NativeDateAdapter { } } + toSaveFormat(value: Date) { + if (!value) { + return undefined; + } + const date = dayjs(value); + if (date.isValid()) { + return date.format(this.saveFormat); + } else { + return undefined; + } + } + /** - * transforms the date to a string representation for display - * @param date date to be formatted - * @param displayFormat format to be used for formatting the date e.g. YYYY-MM-DD - * @returns string representation of the date - */ - format(date: Date, displayFormat: string): string { - return dayjs(date).format(displayFormat); + * transforms the date to a string representation for display + * @param date date to be formatted + * @param displayFormat format to be used for formatting the date e.g. YYYY-MM-DD + * @returns string representation of the date + */ + format(date: Date): string { + return dayjs(date).format(this.displayFormat); } deserialize(value: any): Date | null { @@ -72,5 +99,4 @@ export class DayJsDateAdapter extends NativeDateAdapter { return null; } } - } diff --git a/packages/angular-material/test/date-control.spec.ts b/packages/angular-material/test/date-control.spec.ts index a3ff3f3c88..8531fc8622 100644 --- a/packages/angular-material/test/date-control.spec.ts +++ b/packages/angular-material/test/date-control.spec.ts @@ -113,7 +113,9 @@ describe('Date control Base Tests', () => { ); component.ngOnInit(); fixture.detectChanges(); - expect(component.data).toBe('2018-01-01'); + expect(component.data.toString()).toEqual( + new Date('2018-01-01T00:00').toString() + ); // auto? shown with US layout expect(inputElement.value).toBe('1/1/2018'); expect(inputElement.disabled).toBe(false); @@ -133,7 +135,9 @@ describe('Date control Base Tests', () => { Actions.update('foo', () => '2018-03-03') ); fixture.detectChanges(); - expect(component.data).toBe('2018-03-03'); + expect(component.data.toString()).toEqual( + new Date('2018-03-03T00:00').toString() + ); expect(inputElement.value).toBe('3/3/2018'); }); it('should update with undefined value', () => { @@ -148,7 +152,7 @@ describe('Date control Base Tests', () => { Actions.update('foo', () => undefined) ); fixture.detectChanges(); - expect(component.data).toBe(undefined); + expect(component.data).toBe(null); expect(inputElement.value).toBe(''); }); it('should update with null value', () => { @@ -181,8 +185,10 @@ describe('Date control Base Tests', () => { Actions.update('bar', () => '2018-03-03') ); fixture.detectChanges(); - expect(component.data).toBe('2018-01-01'); - expect(inputElement.value).toBe('1/1/2018'); + expect(component.data.toString()).toEqual( + new Date('2018-01-01T00:00').toString() + ); + expect(inputElement.value).toEqual('1/1/2018'); }); // store needed as we evaluate the calculated enabled value to disable/enable the control it('can be disabled', () => { diff --git a/packages/examples/src/examples/dates.ts b/packages/examples/src/examples/dates.ts index 02816c8b3c..37b8430e01 100644 --- a/packages/examples/src/examples/dates.ts +++ b/packages/examples/src/examples/dates.ts @@ -133,7 +133,7 @@ export const data = { datetime: new Date().toISOString(), }, uiSchemaBased: { - date: new Date().toISOString().substr(0, 10), + date: '2024-01', time: '13:37:00', datetime: '1999/12/11 10:05 am', }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 958bd038db..f8a2c0ba4a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -283,6 +283,9 @@ importers: copy-webpack-plugin: specifier: ^11.0.0 version: 11.0.0(webpack@5.89.0) + dayjs: + specifier: ^1.11.10 + version: 1.11.10 eslint: specifier: ^7.32.0 version: 7.32.0 @@ -15969,6 +15972,13 @@ packages: integrity: sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==, } + /dayjs@1.11.10: + resolution: + { + integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==, + } + dev: true + /debug@2.6.9(supports-color@6.1.0): resolution: {