Asymmetrik/ngx-starter

View on GitHub
src/app/common/datepicker/datepicker-range-popup/datepicker-range-popup.component.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { JsonPipe } from '@angular/common';
import {
    Component,
    computed,
    effect,
    forwardRef,
    inject,
    input,
    signal,
    viewChild
} from '@angular/core';
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';

import {
    NgbCalendar,
    NgbDate,
    NgbDateAdapter,
    NgbDateParserFormatter,
    NgbDatepickerModule,
    NgbInputDatepicker
} from '@ng-bootstrap/ng-bootstrap';

type DateRange = [Date | null, Date | null];

@Component({
    selector: 'app-datepicker-range-popup',
    standalone: true,
    providers: [
        {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => DatepickerRangePopupComponent),
            multi: true
        }
    ],
    imports: [NgbDatepickerModule, FormsModule, JsonPipe],
    templateUrl: './datepicker-range-popup.component.html',
    styleUrl: './datepicker-range-popup.component.scss'
})
export class DatepickerRangePopupComponent implements ControlValueAccessor {
    readonly #calendar = inject(NgbCalendar);
    readonly formatter = inject(NgbDateParserFormatter);
    readonly #adapter = inject(NgbDateAdapter);

    readonly #changed = new Array<(value: DateRange) => void>();
    readonly #touched = new Array<() => void>();

    readonly datepicker = viewChild.required(NgbInputDatepicker);

    readonly disabled = input(false);

    readonly fromDate = signal<NgbDate | null>(null);
    readonly toDate = signal<NgbDate | null>(null);
    readonly hoveredDate = signal<NgbDate | null>(null);

    readonly fromDateString = computed(() => this.formatter.format(this.fromDate()));
    readonly toDateString = computed(() => this.formatter.format(this.toDate()));

    readonly asNativeDateTuple = computed<DateRange>(() => [
        this.#adapter.toModel(this.fromDate()),
        this.#adapter.toModel(this.toDate())
    ]);

    constructor() {
        effect(() => {
            this.propagateChange();
        });
    }

    writeValue(value: DateRange) {
        this.fromDate.set(NgbDate.from(this.#adapter.fromModel(value?.[0])));
        this.toDate.set(NgbDate.from(this.#adapter.fromModel(value?.[1])));
    }

    registerOnChange(fn: (value: DateRange) => void) {
        this.#changed.push(fn);
    }

    registerOnTouched(fn: () => void) {
        this.#touched.push(fn);
    }

    propagateChange() {
        this.#changed.forEach((f) => f(this.asNativeDateTuple()));
    }

    onDateSelection(date: NgbDate) {
        if (!this.fromDate() && !this.toDate()) {
            this.fromDate.set(date);
        } else if (this.fromDate() && !this.toDate() && date && date.after(this.fromDate())) {
            this.toDate.set(date);
            this.datepicker().close();
        } else {
            this.toDate.set(null);
            this.fromDate.set(date);
        }
    }

    isHovered(date: NgbDate) {
        return (
            this.fromDate() &&
            !this.toDate() &&
            this.hoveredDate() &&
            date.after(this.fromDate()) &&
            date.before(this.hoveredDate())
        );
    }

    isInside(date: NgbDate) {
        return this.toDate() && date.after(this.fromDate()) && date.before(this.toDate());
    }

    isRange(date: NgbDate) {
        return (
            date.equals(this.fromDate()) ||
            (this.toDate() && date.equals(this.toDate())) ||
            this.isInside(date) ||
            this.isHovered(date)
        );
    }

    validateInput(currentValue: NgbDate | null, input: string): NgbDate | null {
        const parsed = this.formatter.parse(input);
        return parsed && this.#calendar.isValid(NgbDate.from(parsed))
            ? NgbDate.from(parsed)
            : currentValue;
    }
}