Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dh): watt-date-field + watt-date-range-field #3793

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
3 changes: 2 additions & 1 deletion libs/watt/src/lib/components/button/watt-button.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type WattButtonType = 'button' | 'reset' | 'submit';
selector: 'watt-button',
template: `
<button
tabindex="0"
mat-button
[disabled]="disabled"
[type]="type"
Expand All @@ -49,7 +50,7 @@ export type WattButtonType = 'button' | 'reset' | 'submit';
<div
[ngClass]="{
'content-wrapper--loading': loading,
'content-wrapper': !loading
'content-wrapper': !loading,
}"
>
@if (hasIcon()) {
Expand Down
17 changes: 17 additions & 0 deletions libs/watt/src/lib/components/date-field/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @license
* Copyright 2020 Energinet DataHub A/S
*
* Licensed under the Apache License, Version 2.0 (the "License2");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { WattDateField } from './watt-date-field.component';
203 changes: 203 additions & 0 deletions libs/watt/src/lib/components/date-field/watt-date-field.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
/**
* @license
* Copyright 2020 Energinet DataHub A/S
*
* Licensed under the Apache License, Version 2.0 (the "License2");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
ChangeDetectionStrategy,
Component,
computed,
forwardRef,
inject,
input,
output,
signal,
viewChild,
ViewEncapsulation,
} from '@angular/core';
import {
ControlValueAccessor,
FormControl,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms';
import { MatCalendar } from '@angular/material/datepicker';
import { MaskitoDirective } from '@maskito/angular';
import { maskitoDateOptionsGenerator } from '@maskito/kit';
import { map, share } from 'rxjs';
import { dayjs } from '@energinet-datahub/watt/date';
import { WattFieldComponent } from '../field';
import { WattButtonComponent } from '../button/watt-button.component';
import { outputFromObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { WattLocaleService } from '@energinet-datahub/watt/locale';

const DA_FILLER = 'dd-mm-åååå';
const EN_FILLER = 'dd-mm-yyyy';
const DATE_FORMAT = 'DD-MM-YYYY';
const DANISH_TIME_ZONE_IDENTIFIER = 'Europe/Copenhagen';

/* eslint-disable @angular-eslint/component-class-suffix */
@Component({
standalone: true,
selector: 'watt-date-field',
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => WattDateField),
multi: true,
},
],
imports: [
ReactiveFormsModule,
MaskitoDirective,
MatCalendar,
WattButtonComponent,
WattFieldComponent,
],
styles: [
`
watt-date-field {
display: block;
width: 100%;
}

.watt-date-field-picker {
position-area: bottom span-right;
position-try-fallbacks: flip-block;
inset: unset;
margin: unset;
border: 0;
}
`,
],
template: `
<watt-field #field [label]="label()" [control]="control" [placeholder]="placeholder()">
<input
#input
[formControl]="control"
[maskito]="mask()"
(focus)="picker.showPopover()"
(blur)="handleBlur(picker, $event)"
/>
<watt-button icon="date" variant="icon" (click)="input.focus()" />
<div
#picker
class="watt-calendar watt-date-field-picker"
popover="manual"
tabindex="0"
[style.position-anchor]="field.inputAnchor"
>
<mat-calendar
[startAt]="selected()"
[selected]="selected()"
[minDate]="min()"
[maxDate]="max()"
(selectedChange)="handleSelectedChange(input, picker, $event)"
/>
</div>
<ng-content />
<ng-content select="watt-field-error" ngProjectAs="watt-field-error" />
<ng-content select="watt-field-hint" ngProjectAs="watt-field-hint" />
</watt-field>
`,
})
export class WattDateField implements ControlValueAccessor {
private locale = inject(WattLocaleService);

/** Converts date from outer FormControl to format of inner FormControl. */
protected modelToView = (value: Date | null) =>
value ? dayjs(value).tz(DANISH_TIME_ZONE_IDENTIFIER).format(DATE_FORMAT) : '';

/** Converts value of inner FormControl to type of outer FormControl. */
protected viewToModel = (value: string) => {
const date = dayjs(value, DATE_FORMAT, true);
return date.isValid() ? date.toDate() : null;
};

// Must unfortunately be queried in order to update `activeDate`
private calendar = viewChild.required<MatCalendar<Date>>(MatCalendar);

// This inner FormControl is string only, but the outer FormControl is of type Date.
protected control = new FormControl('', { nonNullable: true });

// `registerOnChange` may subscribe to this component after it has been destroyed, thus
// triggering an NG0911 from the `takeUntilDestroyed` operator. By sharing the observable,
// the observable will already be closed and `subscribe` becomes a proper noop.
private valueChanges = this.control.valueChanges.pipe(
map(this.viewToModel),
takeUntilDestroyed(),
share()
);

/** Set the label text for `watt-field`. */
label = input('');

/** The minimum selectable date. */
min = input<Date>();

/** The maximum selectable date. */
max = input<Date>();

/** Emits when the selected date has changed. */
dateChange = outputFromObservable(this.valueChanges);

/** Emits when the field loses focus. */
blur = output<FocusEvent>();

protected selected = signal<Date | null>(null);
protected placeholder = computed(() => (this.locale.isDanish() ? DA_FILLER : EN_FILLER));
protected mask = computed(() =>
maskitoDateOptionsGenerator({
min: this.min(),
max: this.max(),
mode: 'dd/mm/yyyy',
separator: '-',
})
);

protected handleBlur = (picker: HTMLElement, event: FocusEvent) => {
if (event.relatedTarget instanceof HTMLElement && picker.contains(event.relatedTarget)) {
const target = event.target as HTMLInputElement; // safe type assertion
setTimeout(() => target.focus());
} else {
picker.hidePopover();
this.blur.emit(event);
}
};

protected handleSelectedChange = (
input: HTMLInputElement,
picker: HTMLDivElement,
date: Date
) => {
input.value = this.modelToView(date);
input.dispatchEvent(new Event('input', { bubbles: true }));
picker.hidePopover();
};

constructor() {
this.valueChanges.subscribe((value) => {
this.selected.set(value);
this.calendar().activeDate = value ?? new Date();
});
}

// Implementation for ControlValueAccessor
writeValue = (value: Date | null) => this.control.setValue(this.modelToView(value));
setDisabledState = (x: boolean) => (x ? this.control.disable() : this.control.enable());
registerOnTouched = (fn: () => void) => this.blur.subscribe(fn);
registerOnChange = (fn: (value: Date | null) => void) => this.valueChanges.subscribe(fn);
}
73 changes: 73 additions & 0 deletions libs/watt/src/lib/components/date-field/watt-date-field.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* @license
* Copyright 2020 Energinet DataHub A/S
*
* Licensed under the Apache License, Version 2.0 (the "License2");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { applicationConfig, Meta, moduleMetadata, StoryFn } from '@storybook/angular';
import { WattDateField } from './watt-date-field.component';
import { DateAdapter, MAT_DATE_FORMATS, MAT_NATIVE_DATE_FORMATS } from '@angular/material/core';
import { WattDateAdapter } from '../../configuration/watt-date-adapter';
import { signal } from '@angular/core';

const meta: Meta<WattDateField> = {
title: 'Components/DateField',
component: WattDateField,
decorators: [
moduleMetadata({
imports: [ReactiveFormsModule],
}),
applicationConfig({
providers: [
{ provide: DateAdapter, useClass: WattDateAdapter },
{ provide: MAT_DATE_FORMATS, useValue: MAT_NATIVE_DATE_FORMATS },
],
}),
],
};

export default meta;

const destination = new FormControl<Date | null>(null);
const present = new FormControl(new Date());
const lastDeparted = new FormControl<Date | null>({ value: null, disabled: true });
const min = signal(new Date());
const max = signal(new Date());
const flux = (destination: Date | null) => {
if (!destination) return;
lastDeparted.setValue(present.value);
min.set(destination);
max.set(destination);
setTimeout(() => present.setValue(destination));
};

export const Overview: StoryFn = () => ({
props: { destination, present, lastDeparted, min, max, flux },
template: `
<watt-date-field
label="Destination time"
[formControl]="destination"
(dateChange)="flux($event)"
/>
<watt-date-field
label="Present time"
[min]="min()"
[max]="max()"
[formControl]="present"
/>
<watt-date-field
label="Last time departed"
[formControl]="lastDeparted"
/>`,
});
17 changes: 17 additions & 0 deletions libs/watt/src/lib/components/date-range-field/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* @license
* Copyright 2020 Energinet DataHub A/S
*
* Licensed under the Apache License, Version 2.0 (the "License2");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { WattDateRangeField } from './watt-date-range-field.component';
Loading
Loading