Skip to content

Commit

Permalink
Merge pull request #855 from DSI-HUG/feat/DEJS-1179-add-date-with-tim…
Browse files Browse the repository at this point in the history
…e-picker

Feat/dejs 1179 add date with time picker
  • Loading branch information
vapkse committed Jan 24, 2024
2 parents dae7646 + c0e9092 commit a1ad627
Show file tree
Hide file tree
Showing 18 changed files with 599 additions and 10 deletions.
28 changes: 21 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@angular/forms": "~16.2.0",
"@angular/localize": "~16.2.0",
"@angular/material": "~16.2.0",
"@angular/material-date-fns-adapter": "~16.2.0",
"@angular/platform-browser": "~16.2.0",
"@angular/platform-browser-dynamic": "~16.2.0",
"@angular/router": "~16.2.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* @license
* Copyright Hôpitaux Universitaires de Genève. All Rights Reserved.
*
* Use of this source code is governed by an Apache-2.0 license that can be
* found in the LICENSE file at https://github.com/DSI-HUG/dejajs-components/blob/master/LICENSE
*/

import { InjectionToken } from '@angular/core';

// eslint-disable-next-line @typescript-eslint/naming-convention
export const DATE_TIME_ADAPTER = new InjectionToken<DateTimeAdapter<unknown>>('DATE_TIME_ADAPTER');

export interface DateTimeAdapter<D> {
setTime: (date: D, hours: number, minutes: number, seconds: number) => D;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<ng-template>
<ng-container *ngIf="time">
<deja-time-picker
#timepicker
class="datepicker-with-time"
[(time)]="time"
></deja-time-picker>
<div class="datepicker-with-time action-buttons">
<button
mat-raised-button
color="primary"
matDatepickerApply
(click)="onDateTimeClosed()"
>
OK
</button>
</div>
</ng-container>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
deja-time-picker.datepicker-with-time {
.time-range {
display: flex;
align-items: center;

.time-picker-separator {
text-align: center;
margin: 0 0.5rem;
}
}
}

.datepicker-with-time.action-buttons {
padding: 1.5rem 0 1rem 0;
display: flex;
justify-content: center;
}

.mat-datepicker-content-container[layout="h"] {
flex-direction: row;
flex-wrap: wrap;
justify-content: space-evenly;

.mat-calendar {
height: auto;

.mat-calendar-header {
padding: 0 0.5rem 0 0.5rem;
}
}

.mat-calendar,
deja-time-picker.datepicker-with-time {
flex: 0 0 auto;
}

.datepicker-with-time.action-buttons {
flex: 1 0 100%;
padding: 0 0 1rem 0;
;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
/*
* @license
* Copyright Hôpitaux Universitaires de Genève. All Rights Reserved.
*
* Use of this source code is governed by an Apache-2.0 license that can be
* found in the LICENSE file at https://github.com/DSI-HUG/dejajs-components/blob/master/LICENSE
*/

import { TemplatePortal } from '@angular/cdk/portal';
import { CommonModule } from '@angular/common';
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, inject, OnDestroy, TemplateRef, ViewChild, ViewContainerRef, ViewEncapsulation } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { DateAdapter } from '@angular/material/core';
import { MatDatepicker, MatDatepickerInput, MatDatepickerModule, MatDateSelectionModel } from '@angular/material/datepicker';
import { Destroy } from '@deja-js/component/core';
import { DateOrDuration, DejaTimePickerComponent, DejaTimePickerModule } from '@deja-js/component/time-picker';
import { cloneDeep } from 'lodash-es';
import { delay, filter, map, takeUntil, tap } from 'rxjs';


@Component({
selector: 'datepicker-with-time',
templateUrl: './datepicker-with-time.component.html',
styleUrls: ['./datepicker-with-time.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
standalone: true,
imports: [
CommonModule,
DejaTimePickerModule,
MatButtonModule,
MatDatepickerModule
]
})
export class DatepickerWithTimeComponent extends Destroy implements AfterViewInit, OnDestroy {
@ViewChild(TemplateRef)
private template?: TemplateRef<unknown>;

@ViewChild(DejaTimePickerComponent, { read: ElementRef, static: false })
private timePickerElement?: ElementRef<HTMLElement>;

public time?: DateOrDuration;

private portal?: TemplatePortal;

private dateAdapter = inject(DateAdapter, { optional: true });
private datepicker = inject(MatDatepicker);
private viewContainerRef = inject(ViewContainerRef);

public constructor(
private globalModel: MatDateSelectionModel<unknown, unknown>
) {
super();

this.datepicker.openedStream.pipe(
tap(() => {
const datePickerInput = this.datepicker.datepickerInput as MatDatepickerInput<Date>;
this.time = datePickerInput.value || new Date();
}),
delay(1),
map(() => this.timePickerElement?.nativeElement.parentElement),
filter(Boolean),
takeUntil(this.destroyed$)
).subscribe(datePickerContainer => {
const containerRef = this.viewContainerRef.element.nativeElement as HTMLElement;
datePickerContainer.setAttribute('layout', containerRef?.ownerDocument?.body?.clientHeight <= 500 ? 'h' : 'v');
});
}

public onDateTimeClosed(): void {
let date = cloneDeep(this.globalModel.selection) || new Date() as unknown;
if (this.time) {
let hours: number;
let minutes: number;
let seconds: number;
if (this.time instanceof Date) {
hours = this.time.getHours();
minutes = this.time.getMinutes();
seconds = this.time.getSeconds();
} else {
hours = this.time.hours || 0;
minutes = this.time.minutes || 0;
seconds = this.time.seconds || 0;
}
if (date instanceof Date) {
date.setHours(hours, minutes, seconds);
} else if (this.dateAdapter && 'setTime' in this.dateAdapter && typeof this.dateAdapter.setTime === 'function') {
date = this.dateAdapter.setTime(date, hours, minutes, seconds);
}
}
this.globalModel.updateSelection(date, this);
const datePickerInput = this.datepicker.datepickerInput as MatDatepickerInput<unknown>;
datePickerInput.writeValue(date);
}

public ngAfterViewInit(): void {
if (this.template) {
this.portal = new TemplatePortal(this.template, this.viewContainerRef);
}

if (this.portal) {
this.datepicker.registerActions(this.portal);
}
}

public ngOnDestroy(): void {
super.ngOnDestroy();

if (this.portal) {
this.datepicker.removeActions(this.portal);
}

// Needs to be null checked since we initialize it in `ngAfterViewInit`.
if (this.portal?.isAttached) {
this.portal?.detach();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* @license
* Copyright Hôpitaux Universitaires de Genève. All Rights Reserved.
*
* Use of this source code is governed by an Apache-2.0 license that can be
* found in the LICENSE file at https://github.com/DSI-HUG/dejajs-components/blob/master/LICENSE
*/

export * from './date-time-adapter';
export * from './datepicker-with-time.component';
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"lib": {
"entryFile": "./index.ts"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Time Picker
Composant pour sélectionner une date et une heure. Il doit être utilisé avec le composant MatDatepicker d'angular Material.

### Utilisation

Pour pouvoir utiliser le composant, il faut qu'il puisse utiliser un `DateAdapter` supportant les heures. L'interface `DateTimeAdapter` défini la méthode `setTime` que votre `DateAdapter` devra supporter afin que le composant fonctionne correctement.

```typescript
export interface DateTimeAdapter<D> {
setTime: (date: D, hours: number, minutes: number, seconds: number) => D;
}
```

Par ailleurs, il faudra également fournit les formats d'affichage et d'interprétation permettant d'afficher les informations `time`, les formats fournit par les modules `DateAdapter` fournit par `@angular/material` ne permettent pas leur utilisation. Il faudra ainsi fournir à votre module ou composant une valeur pour le token `MAT_DATE_FORMATS`.

Vous trouverez ci-dessous un example de configuration pour un module et un composant :

```typescript

const myDateTimeFormats = {
parse: {
dateInput: 'dd.MM.yyyy HH:mm'
},
display: {
dateInput: 'dd.MM.yyyy HH:mm',
monthYearLabel: 'MMM yyyy',
dateA11yLabel: 'dd.MM.yyyy HH:mm',
monthYearA11yLabel: 'MMMM YYYY'
}
};

@NgModule({
...,
imports: [
...,
DateWithTimePickerComponent,
...
],
providers: [
{ provide: DateAdapter, useClass: MyDateTimeAdapter },
{ provide: MAT_DATE_FORMATS, useValue: myDateTimeFormats }
],
...
})
export class SampleModule {}
```

ou au niveau du composant :

```typescript

const myDateTimeFormats = {
parse: {
dateInput: 'dd.MM.yyyy HH:mm'
},
display: {
dateInput: 'dd.MM.yyyy HH:mm',
monthYearLabel: 'MMM yyyy',
dateA11yLabel: 'dd.MM.yyyy HH:mm',
monthYearA11yLabel: 'MMMM YYYY'
}
};

@Component({
...,
providers: [
{ provide: DateAdapter, useClass: MyDateTimeAdapter },
{ provide: MAT_DATE_FORMATS, useValue: myDateTimeFormats }
]
})
export class SampleComponent {}
```
> Ne pas oublier d'importer le `DateWithTimePickerComponent` dans les `imports` et de fournir `MAT_DATE_FORMATS` et `DateApapter` dans les `providers` de vos modules et/ou composants concernés !
Ensuite utiliser le composant comme ceci dans votre template :

```html
<mat-datepicker #dateTimePicker>
<datepicker-with-time></datepicker-with-time>
</mat-datepicker>
```
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, Even
import { ControlValueAccessor, NgControl } from '@angular/forms';
import { MatLegacyFormFieldAppearance as MatFormFieldAppearance } from '@angular/material/legacy-form-field';
import { Destroy } from '@deja-js/component/core';
import { isSameHour, set } from 'date-fns';
import { Duration, isSameHour, set } from 'date-fns';
import { debounce, distinctUntilChanged, map, Subject, takeUntil, timer } from 'rxjs';

export type TimePickerDisplayMode = 'fullTime' | 'fullTimeWithHoursDisabled' | 'fullTimeWithMinutesDisabled' | 'hoursOnly' | 'minutesOnly';
Expand Down Expand Up @@ -241,14 +241,14 @@ export class DejaTimePickerComponent extends Destroy implements ControlValueAcce
}

public get hoursValue(): number | undefined {
if (!this.value || !this.control || (this.forceNullValue && this.mode === 'fullTimeWithMinutesDisabled' && this.control.pristine)) {
if (!this.value || (this.forceNullValue && this.mode === 'fullTimeWithMinutesDisabled' && (!this.control || this.control.pristine))) {
return undefined;
}
return this.value instanceof Date ? this.value.getHours() : this.value.hours;
}

public get minutesValue(): number | undefined {
if (!this.value || !this.control || (this.forceNullValue && this.mode === 'fullTimeWithHoursDisabled' && this.control.pristine)) {
if (!this.value || (this.forceNullValue && this.mode === 'fullTimeWithHoursDisabled' && (!this.control || this.control.pristine))) {
return undefined;
}
return this.value instanceof Date ? this.value.getMinutes() : this.value.minutes;
Expand Down
Loading

0 comments on commit a1ad627

Please sign in to comment.