DateFormatter — унифицированная система форматирования дат и времени. Она обеспечивает единообразное отображение во всех частях приложения и соответствует корпоративным стандартам.

DateFormatter автоматически отслеживает изменения локали через KbqLocaleService и обновляет форматы при смене языка интерфейса.

Методы DateFormatter позволяют форматировать дату и время непосредственно в TypeScript-коде:

const formattedStringOfDate = this.formatter.absoluteLongDate(this.adapter.today());

Для форматирования в HTML-шаблонах предназначены pipe, названия которых соответствуют методам DateFormatter:

<div>{{ adapter.today() | kbqAbsoluteLongDate }}</div>

Чтобы pipe работали, нужно импортировать KbqFormattersModule — он предоставляет DateFormatter и экспортирует все pipe. Примеры использования

Один и тот же формат доступен в трёх вариантах:

Семейство Пример Поведение
kbq*рекомендуется kbqAbsoluteLongDate Пересчитывается при смене локали через KbqLocaleService, результат кэшируется по значению, аргументам и локали
Без префикса absoluteLongDate Pure pipe. Пересчитывается только при изменении входного значения — при смене локали строка не обновится
С суффиксом ImpurePipe absoluteLongDateImpurePipe Impure pipe без кэша: форматирует заново на каждом цикле проверки изменений

Беспрефиксные и ImpurePipe-варианты сохранены для обратной совместимости. В новом коде используйте kbq*.

Pipe Входное значение Аргументы Метод DateFormatter
kbqAbsoluteShortDate дата currYear?: boolean absoluteShortDate
kbqAbsoluteLongDate дата currYear?: boolean absoluteLongDate
kbqAbsoluteShortDateTime дата options?: DateTimeOptions absoluteShortDateTime
kbqAbsoluteLongDateTime дата options?: DateTimeOptions absoluteLongDateTime
kbqRelativeShortDate дата relativeShortDate
kbqRelativeLongDate дата relativeLongDate
kbqRelativeShortDateTime дата options?: DateTimeOptions relativeShortDateTime
kbqRelativeLongDateTime дата options?: DateTimeOptions relativeLongDateTime
kbqRangeShortDate [от, до] rangeShortDate
kbqRangeLongDate [от, до] rangeLongDate
kbqRangeShortDateTime [от, до] options?: DateTimeOptions rangeShortDateTime
kbqRangeMiddleDateTime [от, до] options?: DateTimeOptions rangeMiddleDateTime
kbqRangeLongDateTime [от, до] options?: DateTimeOptions rangeLongDateTime
kbqDurationShortest [от, до] options?: DateTimeOptions durationShortest
kbqDurationShort [от, до] units?: DurationUnit[], fraction?: boolean durationShort
kbqDurationLong [от, до] units?: DurationUnit[], fraction?: boolean durationLong

DateTimeOptions — это { seconds?: boolean; milliseconds?: boolean; currYear?: boolean }. kbqDurationShortest использует из него только seconds (по умолчанию true) и milliseconds.

<div>{{ [task.startedAt, task.finishedAt] | kbqDurationShortest }}</div>
<div>{{ [task.startedAt, task.finishedAt] | kbqDurationLong: ['hours', 'minutes'] }}</div>

Передайте null вместо одной из границ — pipe диапазона сам переключится на формат открытого диапазона («С 15 января», «По 20 июня»). Отдельного pipe для этого не требуется.

<div>{{ [filter.from, filter.to] | kbqRangeLongDate }}</div>

Исключение — kbqRangeMiddleDateTime: у среднего формата нет шаблона открытого диапазона, поэтому он требует обе границы.

Если дату не удалось разобрать или она отсутствует, pipe выводит пустую строку. Это отражено в типах: ещё не заполненная привязка — это null или undefined, а не дата, и для pipe диапазонов и продолжительности это относится как к самому кортежу [от, до], так и к отдельной границе в нём.

Pipe диапазонов, поддерживающие открытый диапазон, — kbqRangeShortDate, kbqRangeLongDate, kbqRangeShortDateTime, kbqRangeLongDateTime — выводят пустую строку, только если обе границы отсутствуют или некорректны; если задана хотя бы одна граница, они переключаются на формат открытого диапазона. kbqRangeMiddleDateTime и pipe продолжительности требуют обе границы, поэтому выводят пустую строку, если отсутствует или некорректна хотя бы одна из них; pipe продолжительности, кроме того, выводят пустую строку, если начало позже конца.

Если нужно узнать об ошибке, а не скрыть её, вызывайте методы DateFormatter напрямую — они бросают исключение.

Форматы, которых нет среди pipe, доступны через DateFormatter: у него есть публичное поле config с шаблонами текущей локали.

private readonly formatter = inject<DateFormatter<DateTime>>(DateFormatter);

format(from: DateTime, to: DateTime): string {
    return this.formatter.rangeDate(from, to, this.formatter.config.rangeTemplates.closedRange.middle);
}

Так же работают absoluteDate, relativeDate, rangeDateTime, duration и openedRangeDate — они принимают шаблон аргументом.

По умолчанию даты показываются в часовом поясе среды, где выполняется код: в браузере — в поясе пользователя, при серверном рендеринге — в поясе сервера. Токен KBQ_DATE_TIMEZONE задаёт пояс один раз для всего приложения — в нём начинают работать pipe, DateFormatter, календарь и поля ввода дат, поэтому передавать смещение в каждый pipe не нужно.

bootstrapApplication(App, {
    providers: [kbqDateTimezoneProvider('Europe/Moscow')]
});

Токен принимает:

Значение Пример Комментарий
Имя пояса IANA 'Europe/Moscow' Учитывает переходы на летнее время
Смещение в минутах от UTC 180, -330 Фиксированное, без перехода на летнее время
Смещение строкой '+03:00', 'UTC+3', 'GMT+05:30' То же, в текстовом виде
'utc'
'system' Значение по умолчанию — пояс среды

Неизвестное имя пояса не ломает отрисовку: даты остаются в поясе среды, а в режиме разработки выводится предупреждение. То же касается смещения за пределами ±14:00 — самого широкого из существовавших; дробное смещение округляется до целых минут.

KbqDateTimezoneService.setTimezone() меняет пояс без перезагрузки страницы, kbq* pipe перерисовываются:

private readonly timezoneService = inject(KbqDateTimezoneService);

setUserTimezone(timezone: string) {
    this.timezoneService.setTimezone(timezone);
}

Pure pipe (без префикса kbq) на смену пояса не реагируют — ровно так же, как и на смену локали.

Пояс приходит к pipe через адаптер дат и DateFormatter, а те берут KbqDateTimezoneService из инжектора, в котором были созданы. kbqDateTimezoneProvider объявляет этот сервис вместе с токеном, поэтому для поддерева его достаточно перечислить рядом с адаптером и форматтером:

@Component({
    providers: [
        kbqDateTimezoneProvider('Asia/Tokyo'),
        { provide: DateAdapter, useClass: LuxonDateAdapter },
        DateFormatter
    ]
})

При SSR страница отрисовывается в поясе сервера, а после гидратации — в поясе браузера. Если значения различаются, все даты на странице пересчитываются, и это видно как мигание. Чтобы этого не было, сервер и клиент должны получить одно и то же значение KBQ_DATE_TIMEZONE.

Надёжнее всего передать пояс через TransferState: сервер берёт его из куки (или из профиля пользователя) и кладёт в состояние, а клиент читает готовое значение — тогда оно совпадает даже в том случае, когда куки нет.

// конфигурация сервера
export const serverConfig = mergeApplicationConfig(appConfig, {
    providers: [
        provideServerRendering(),
        {
            provide: KBQ_DATE_TIMEZONE,
            useFactory: () => {
                const timezone = readTimezoneCookie(inject(REQUEST, { optional: true })) ?? 'utc';

                inject(TransferState).set(TIMEZONE_KEY, timezone);

                return timezone;
            }
        }
    ]
});

// конфигурация браузера — берём ровно то, чем рендерил сервер
export const appConfig: ApplicationConfig = {
    providers: [
        {
            provide: KBQ_DATE_TIMEZONE,
            useFactory: () => inject(TransferState).get(TIMEZONE_KEY, 'utc')
        }
    ]
};

На первом визите куки ещё нет, и страница отрисуется с запасным значением ('utc' в примере). После гидратации сравните его с Intl.DateTimeFormat().resolvedOptions().timeZone, запишите куку и, если пояс нужно применить сразу, вызовите setTimezone() — это одна перерисовка и только на первом визите. Если пояс хранится в профиле пользователя на сервере, первый визит тоже обойдётся без неё.

Предложения по улучшению
Если вы нашли ошибку или хотите доработать статью, создайте запрос на GitHub.