Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions packages/components-dev/date-pipes/module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import { ChangeDetectionStrategy, Component, inject, ViewEncapsulation } from '@angular/core';
import { KbqLuxonDateModule } from '@koobiq/angular-luxon-adapter/adapter';
import { DateAdapter, DateFormatter, KbqFormattersModule, KbqLocaleService } from '@koobiq/components/core';
import {
DateAdapter,
DateFormatter,
KBQ_LOCALE_SERVICE,
KbqFormattersModule,
KbqLocaleService
} from '@koobiq/components/core';
import { DateTime } from 'luxon';

@Component({
Expand All @@ -14,7 +20,9 @@ import { DateTime } from 'luxon';
export class DevApp {
protected readonly dateAdapter: DateAdapter<DateTime> = inject(DateAdapter<DateTime>);
protected readonly formatter: DateFormatter<DateTime> = inject(DateFormatter<DateTime>);
protected readonly localeService: KbqLocaleService = inject(KbqLocaleService);
// The token, not the class: `KbqLuxonDateModule` provides it with `useClass`, so the `providedIn: 'root'`
// instance is a different object from the one the pipes and `DateFormatter` subscribe to.
protected readonly localeService: KbqLocaleService = inject(KBQ_LOCALE_SERVICE);

obj: any = {
absolute: {
Expand Down Expand Up @@ -100,6 +108,15 @@ export class DevApp {
endsNotCurrentYear: ''
}
}
},
duration: {
seconds: '',
minutesSeconds: '',
hoursMinutes: '',
daysHours: '',
weeksDays: '',
monthsWeeks: '',
yearsMonths: ''
}
};

Expand All @@ -114,6 +131,30 @@ export class DevApp {
this.populateRangeLong();
this.populateRangeMiddle();
this.populateRangeShort();
this.populateDuration();
}

/** Switches between the two locales so the difference between the pipe families is visible. */
toggleLocale() {
this.localeService.setLocale(this.localeService.id === 'ru-RU' ? 'en-US' : 'ru-RU');
}

populateDuration() {
const start = this.dateAdapter.today().set({ hour: 0, minute: 0, second: 0, millisecond: 0 });
const ranges: Record<string, DateTime> = {
seconds: start.plus({ seconds: 21, milliseconds: 365 }),
minutesSeconds: start.plus({ minutes: 1, seconds: 25 }),
hoursMinutes: start.plus({ hours: 1, minutes: 21 }),
daysHours: start.plus({ days: 1, hours: 8, minutes: 25 }),
weeksDays: start.plus({ days: 15 }),
monthsWeeks: start.plus({ months: 1, days: 25 }),
yearsMonths: start.plus({ years: 3, months: 11 })
};

Object.entries(ranges).forEach(([key, end]) => {
this.obj.duration[key] = [start, end];
this.iso.duration[key] = [this.dateAdapter.toIso8601(start), this.dateAdapter.toIso8601(end)];
});
}

populateRangeShort() {
Expand Down
131 changes: 131 additions & 0 deletions packages/components-dev/date-pipes/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
<br />
<br />
<header class="kbq-display-compact">pipe's</header>
<div class="dev-container">
<button (click)="toggleLocale()">Toggle locale (current: {{ localeService.id }})</button>
<p>
Only the
<code>kbq*</code>
pipes below re-render on a locale change. The unprefixed ones are pure and stay as they were rendered first.
</p>
</div>
<br />
<header class="kbq-display-compact">Absolute date</header>
<div class="dev-container">
Expand Down Expand Up @@ -656,4 +664,127 @@ <h3>Short format</h3>
</div>
</div>
</div>

<br />
<header class="kbq-display-compact">Duration</header>
<div class="dev-container">
<h3>Shortest format</h3>
<div>
<div class="layout-row dev-row-border">
<div class="flex dev-light-text-secondary">Name</div>
<div class="flex dev-light-text-secondary">Default locale</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationShortest (seconds)</div>
<div class="flex">{{ obj.duration.seconds | kbqDurationShortest }}</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationShortest (seconds) (with milliseconds)</div>
<div class="flex">
{{ obj.duration.seconds | kbqDurationShortest: { seconds: true, milliseconds: true } }}
</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationShortest (minutes and seconds)</div>
<div class="flex">{{ obj.duration.minutesSeconds | kbqDurationShortest }}</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationShortest (minutes and seconds) (without seconds)</div>
<div class="flex">{{ obj.duration.minutesSeconds | kbqDurationShortest: { seconds: false } }}</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationShortest (days and hours)</div>
<div class="flex">{{ obj.duration.daysHours | kbqDurationShortest }}</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationShortest (from ISO strings)</div>
<div class="flex">{{ iso.duration.daysHours | kbqDurationShortest }}</div>
</div>
</div>
</div>
<div class="dev-container">
<h3>Long format</h3>
<div>
<div class="layout-row dev-row-border">
<div class="flex dev-light-text-secondary">Name</div>
<div class="flex dev-light-text-secondary">Default locale</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationLong (seconds)</div>
<div class="flex">{{ obj.duration.seconds | kbqDurationLong }}</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationLong (minutes and seconds)</div>
<div class="flex">{{ obj.duration.minutesSeconds | kbqDurationLong }}</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationLong (hours and minutes)</div>
<div class="flex">{{ obj.duration.hoursMinutes | kbqDurationLong }}</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationLong (days and hours)</div>
<div class="flex">{{ obj.duration.daysHours | kbqDurationLong }}</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationLong (days and hours) (only hours)</div>
<div class="flex">{{ obj.duration.daysHours | kbqDurationLong: ['hours'] }}</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationLong (weeks and days)</div>
<div class="flex">{{ obj.duration.weeksDays | kbqDurationLong }}</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationLong (months and weeks)</div>
<div class="flex">{{ obj.duration.monthsWeeks | kbqDurationLong }}</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationLong (years and months)</div>
<div class="flex">{{ obj.duration.yearsMonths | kbqDurationLong }}</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationLong (years and months) (only years, with fraction)</div>
<div class="flex">{{ obj.duration.yearsMonths | kbqDurationLong: ['years'] : true }}</div>
</div>
<div class="dev-row-border"></div>
<div class="layout-row dev-row-border">
<div class="flex">durationLong (pure, stale after a locale change)</div>
<div class="flex">{{ obj.duration.yearsMonths | durationLong }}</div>
</div>
</div>
</div>
<div class="dev-container">
<h3>Short format</h3>
<div>
<div class="layout-row dev-row-border">
<div class="flex dev-light-text-secondary">Name</div>
<div class="flex dev-light-text-secondary">Default locale</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationShort (seconds)</div>
<div class="flex">{{ obj.duration.seconds | kbqDurationShort }}</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationShort (seconds) (with milliseconds)</div>
<div class="flex">
{{ obj.duration.seconds | kbqDurationShort: ['seconds', 'milliseconds'] }}
</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationShort (hours and minutes)</div>
<div class="flex">{{ obj.duration.hoursMinutes | kbqDurationShort }}</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationShort (days and hours)</div>
<div class="flex">{{ obj.duration.daysHours | kbqDurationShort }}</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationShort (weeks and days)</div>
<div class="flex">{{ obj.duration.weeksDays | kbqDurationShort }}</div>
</div>
<div class="layout-row dev-row-border">
<div class="flex">kbqDurationShort (years and months)</div>
<div class="flex">{{ obj.duration.yearsMonths | kbqDurationShort }}</div>
</div>
</div>
</div>
</div>
89 changes: 84 additions & 5 deletions packages/components/core/formatters/date/date-formatter.en.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,96 @@
To format dates, you need to use DateFormatter methods,
for example:
DateFormatter is a unified system for formatting dates and times. It keeps the presentation consistent across the whole application and follows the corporate standards.

DateFormatter tracks locale changes through KbqLocaleService on its own and updates the formats when the interface language changes.

### Methods in TypeScript code

DateFormatter methods format a date or a time directly in TypeScript code:

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

You can also use pipe:
### Pipes in templates

Formatting in HTML templates is done with pipes whose names correspond to the DateFormatter methods:

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

The pipes need `KbqFormattersModule` to be imported — it provides `DateFormatter` and exports every pipe. [Usage examples](https://github.com/koobiq/angular-components/tree/main/packages/components-dev/date-pipes)

#### Which family to choose

The same format is available in three flavours:

| Family | Example | Behaviour |
| ------------------------ | ---------------------------- | -------------------------------------------------------------------------------------------------------- |
| `kbq*` — **recommended** | `kbqAbsoluteLongDate` | Recomputes on a KbqLocaleService locale change, caches the result by value, arguments and locale |
| No prefix | `absoluteLongDate` | A pure pipe. Recomputes only when the input value changes — **the string goes stale on a locale change** |
| `ImpurePipe` suffix | `absoluteLongDateImpurePipe` | An impure pipe with no cache: reformats on every change detection cycle |

The unprefixed and `ImpurePipe` flavours are kept for backward compatibility. Use `kbq*` in new code.

#### The pipes

| Pipe | Input | Arguments | DateFormatter method |
| -------------------------- | ------------ | ---------------------------------------------- | ----------------------- |
| `kbqAbsoluteShortDate` | date | `currYear?: boolean` | `absoluteShortDate` |
| `kbqAbsoluteLongDate` | date | `currYear?: boolean` | `absoluteLongDate` |
| `kbqAbsoluteShortDateTime` | date | `options?: DateTimeOptions` | `absoluteShortDateTime` |
| `kbqAbsoluteLongDateTime` | date | `options?: DateTimeOptions` | `absoluteLongDateTime` |
| `kbqRelativeShortDate` | date | — | `relativeShortDate` |
| `kbqRelativeLongDate` | date | — | `relativeLongDate` |
| `kbqRelativeShortDateTime` | date | `options?: DateTimeOptions` | `relativeShortDateTime` |
| `kbqRelativeLongDateTime` | date | `options?: DateTimeOptions` | `relativeLongDateTime` |
| `kbqRangeShortDate` | `[from, to]` | — | `rangeShortDate` |
| `kbqRangeLongDate` | `[from, to]` | — | `rangeLongDate` |
| `kbqRangeShortDateTime` | `[from, to]` | `options?: DateTimeOptions` | `rangeShortDateTime` |
| `kbqRangeMiddleDateTime` | `[from, to]` | `options?: DateTimeOptions` | `rangeMiddleDateTime` |
| `kbqRangeLongDateTime` | `[from, to]` | `options?: DateTimeOptions` | `rangeLongDateTime` |
| `kbqDurationShortest` | `[from, to]` | `options?: DateTimeOptions` | `durationShortest` |
| `kbqDurationShort` | `[from, to]` | `units?: DurationUnit[]`, `fraction?: boolean` | `durationShort` |
| `kbqDurationLong` | `[from, to]` | `units?: DurationUnit[]`, `fraction?: boolean` | `durationLong` |

`DateTimeOptions` is `{ seconds?: boolean; milliseconds?: boolean; currYear?: boolean }`. `kbqDurationShortest` uses only `seconds` (`true` by default) and `milliseconds` from it.

```html
<div>{{ adapter.today() | absoluteLongDate }}</div>
<div>{{ [task.startedAt, task.finishedAt] | kbqDurationShortest }}</div>
<div>{{ [task.startedAt, task.finishedAt] | kbqDurationLong: ['hours', 'minutes'] }}</div>
```

#### Opened ranges

Pass `null` instead of one of the bounds and the range pipe switches to the opened-range format ("From January 15", "Until June 20") on its own. No separate pipe is needed for that.

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

`kbqRangeMiddleDateTime` is the exception: the middle format has no opened-range template, so it requires both bounds.

#### Empty and invalid values

When a date is missing or cannot be parsed, the pipe renders an empty string. The input types allow this: a binding that has not been populated yet is `null` or `undefined`, not a date, and for the range and duration pipes that applies to the whole `[from, to]` tuple as well as to a bound inside it.

The range pipes that support an opened range — `kbqRangeShortDate`, `kbqRangeLongDate`, `kbqRangeShortDateTime`, `kbqRangeLongDateTime` — render an empty string only when both bounds are missing or invalid; a single valid bound switches them to the opened-range format instead. `kbqRangeMiddleDateTime` and the duration pipes need both bounds, so they render an empty string as soon as either one is missing or invalid; the duration pipes additionally render an empty string when the start is later than the end.

Call the `DateFormatter` methods directly if you need to be told about the error instead of hiding it — they throw.

#### Custom formats

Formats that have no pipe are available through `DateFormatter`: its public `config` field holds the templates of the active locale.

```typescript
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);
}
```

The pipe name (absoluteLongDate) corresponds to the name of the DateFormatter method. Examples of usage can be found [here](https://github.com/koobiq/angular-components/tree/main/packages/components-dev/date-pipes)
`absoluteDate`, `relativeDate`, `rangeDateTime`, `duration` and `openedRangeDate` work the same way — they take a template as an argument.

### Absolute date

Expand Down
Loading