import type { BooleanInput } from '@angular/cdk/coercion';
import {
booleanAttribute,
ChangeDetectionStrategy,
Component,
computed,
contentChild,
forwardRef,
input,
linkedSignal,
output,
signal,
untracked,
viewChild,
} from '@angular/core';
import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
type BrnDatePickerBase,
BrnDatePickerTriggerToken,
provideBrnDatePicker,
} from '@spartan-ng/brain/date-picker';
import { BrnFieldControl, provideBrnLabelable } from '@spartan-ng/brain/field';
import type { ChangeFn, TouchFn } from '@spartan-ng/brain/forms';
import type { BrnOverlayState } from '@spartan-ng/brain/overlay';
import { BrnPopover, type BrnPopoverAlign } from '@spartan-ng/brain/popover';
import { HlmCalendarRange } from '@spartan-ng/styles/calendar';
import { HlmPopoverImports } from '@spartan-ng/styles/popover';
import { injectHlmDateRangePickerConfig } from './hlm-date-range-picker.token';
export const HLM_DATE_RANGE_PICKER_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => HlmDateRangePicker),
multi: true,
};
@Component({
selector: 'hlm-date-range-picker',
imports: [HlmPopoverImports, HlmCalendarRange],
providers: [
HLM_DATE_RANGE_PICKER_VALUE_ACCESSOR,
provideBrnDatePicker(HlmDateRangePicker),
provideBrnLabelable(HlmDateRangePicker),
],
changeDetection: ChangeDetectionStrategy.OnPush,
hostDirectives: [BrnFieldControl],
host: { class: 'block' },
template: `
`,
})
export class HlmDateRangePicker implements BrnDatePickerBase<[T, T]>, ControlValueAccessor {
private readonly _config = injectHlmDateRangePickerConfig();
public readonly popover = viewChild.required(BrnPopover);
private readonly _trigger = contentChild(BrnDatePickerTriggerToken);
public readonly align = input('center');
/** Show dropdowns to navigate between months or years. */
public readonly captionLayout = input<
'dropdown' | 'label' | 'dropdown-months' | 'dropdown-years'
>('label');
/** The minimum date that can be selected.*/
public readonly minDate = input();
/** The maximum date that can be selected. */
public readonly maxDate = input();
/** Determine if the date picker is disabled. */
public readonly disabled = input(false, {
transform: booleanAttribute,
});
/** The selected value. */
public readonly date = input<[T, T]>();
protected readonly _mutableDate = linkedSignal(this.date);
protected readonly _start = linkedSignal(() => this._mutableDate()?.[0]);
protected readonly _end = linkedSignal(() => this._mutableDate()?.[1]);
/** If true, the date picker will close when the end date is selected */
public readonly autoCloseOnEndSelection = input(
this._config.autoCloseOnEndSelection,
{
transform: booleanAttribute,
},
);
/** Defines how the date should be displayed in the UI. */
public readonly formatDates = input<(dates: [T | null, T | null]) => string>(
this._config.formatDates,
);
/** Defines how the date should be transformed before saving to model/form. */
public readonly transformDates = input<(date: [T, T]) => [T, T]>(this._config.transformDates);
protected readonly _popoverState = signal(null);
protected readonly _disabled = linkedSignal(this.disabled);
/** @internal The disabled state as a readonly signal */
public readonly disabledState = this._disabled.asReadonly();
public readonly formattedDate = computed(() => {
const start = this._start();
const end = this._end();
return start || end ? this.formatDates()([start ?? null, end ?? null]) : undefined;
});
public readonly dateChange = output<[T, T] | null>();
public readonly labelableId = computed(() => this._trigger()?.triggerId());
public readonly hasDate = computed(() => !!this._start() || !!this._end());
/** @internal The current raw value, used by inputs to reformat on focus. */
public readonly value = computed(() => this._mutableDate() ?? null);
protected _onChange?: ChangeFn<[T, T] | null>;
protected _onTouched?: TouchFn;
protected _onStateChange(state: BrnOverlayState) {
this._popoverState.set(state);
if (state === 'closed') {
this._onClose();
this._onTouched?.();
}
}
protected _handleStartDayChange(value: T | undefined) {
this._start.set(value);
}
protected _handleEndDateChange(value: T | undefined): void {
this._end.set(value);
if (this._disabled()) return;
const start = this._start();
if (start && value) {
const transformedDates = this.transformDates()([start, value]);
this._mutableDate.set(transformedDates);
this.dateChange.emit(transformedDates);
this._onChange?.(transformedDates);
if (this.autoCloseOnEndSelection()) {
this._popoverState.set('closed');
}
}
}
/**
* Commit a range to the picker. Updates the internal model, notifies form
* controls, and emits `dateChange`. Intended to be called from a text input
* that parses user-entered values. Pass `null` to clear the range.
*/
public updateDate(value: [T, T] | null) {
if (this._disabled()) return;
if (!value) {
this._mutableDate.set(undefined);
this._start.set(undefined);
this._end.set(undefined);
this._onChange?.(null);
this.dateChange.emit(null);
return;
}
const transformedDates = this.transformDates()(value);
this._mutableDate.set(transformedDates);
this._start.set(transformedDates[0]);
this._end.set(transformedDates[1]);
this._onChange?.(transformedDates);
this.dateChange.emit(transformedDates);
}
public touched(): void {
this._onTouched?.();
}
/** CONTROL VALUE ACCESSOR */
public writeValue(value: [T, T] | null): void {
untracked(() => {
if (!value) {
this._mutableDate.set(undefined);
} else {
this._mutableDate.set(this.transformDates()(value));
}
});
}
public registerOnChange(fn: ChangeFn<[T, T] | null>): void {
this._onChange = fn;
}
public registerOnTouched(fn: TouchFn): void {
this._onTouched = fn;
}
public setDisabledState(isDisabled: boolean): void {
this._disabled.set(isDisabled);
}
public open() {
this._popoverState.set('open');
}
public close() {
this._popoverState.set('closed');
}
public reset() {
this._mutableDate.set(undefined);
this._start.set(undefined);
this._end.set(undefined);
this._onChange?.(null);
this.dateChange.emit(null);
}
protected _onClose(): void {
const dates = this._mutableDate();
if (this._start() && !this._end() && dates) {
this._start.set(dates[0]);
this._end.set(dates[1]);
}
}
}