Skip to content

Commit

Permalink
feat(common): add injection token for default DatePipe configuration (#…
Browse files Browse the repository at this point in the history
…47157)

This commit introduces a new `DATE_PIPE_DEFAULT_OPTIONS` token, which
can be used to configure default DatePipe options, such as date
format and timezone.

DEPRECATED:

The `DATE_PIPE_DEFAULT_TIMEZONE` token is now deprecated in favor
of the `DATE_PIPE_DEFAULT_OPTIONS` token, which accepts an object
as a value and the timezone can be defined as a field (called `timezone`)
on that object.

PR Close #47157
  • Loading branch information
matthiasweiss authored and thePunderWoman committed Oct 7, 2022
1 parent a792bf1 commit bdb5371
Show file tree
Hide file tree
Showing 8 changed files with 192 additions and 14 deletions.
2 changes: 2 additions & 0 deletions aio/content/guide/deprecations.md
Expand Up @@ -88,6 +88,7 @@ v15 - v18
| `@angular/service-worker` | [`SwUpdate#available`](api/service-worker/SwUpdate#available) | <!-- v13 --> v16 |
| template syntax | [`/deep/`, `>>>`, and `::ng-deep`](#deep-component-style-selector) | <!-- v7 --> unspecified |
| template syntax | [`bind-`, `on-`, `bindon-`, and `ref-`](#bind-syntax) | <!-- v13 --> v15 |
| `@angular/common` | [`DatePipe` - `DATE_PIPE_DEFAULT_TIMEZONE`](api/common/DATE_PIPE_DEFAULT_TIMEZONE) | <!-- v15 --> v17 |

For information about Angular CDK and Angular Material deprecations, see the [changelog](https://github.com/angular/components/blob/main/CHANGELOG.md).

Expand All @@ -110,6 +111,7 @@ In the [API reference section](api) of this site, deprecated APIs are indicated
|:--- |:--- |:--- |:--- |
| [`CurrencyPipe` - `DEFAULT_CURRENCY_CODE`](api/common/CurrencyPipe#currency-code-deprecation) | `{provide: DEFAULT_CURRENCY_CODE, useValue: 'USD'}` | v9 | From v11 the default code will be extracted from the locale data given by `LOCALE_ID`, rather than `USD`. |
| [`NgComponentOutlet.ngComponentOutletNgModuleFactory`](api/common/NgComponentOutlet) | `NgComponentOutlet.ngComponentOutletNgModule` | v14 | Use the `ngComponentOutletNgModule` input instead. This input doesn't require resolving NgModule factory. |
| [`DatePipe` - `DATE_PIPE_DEFAULT_TIMEZONE`](api/common/DATE_PIPE_DEFAULT_TIMEZONE) |`{ provide: DATE_PIPE_DEFAULT_OPTIONS, useValue: { timezone: '-1200' }` | v15 | Use the `DATE_PIPE_DEFAULT_OPTIONS` injection token, which can configure multiple settings at once instead. |

<a id="common-http"></a>

Expand Down
15 changes: 13 additions & 2 deletions goldens/public-api/common/index.md
Expand Up @@ -76,23 +76,34 @@ export class CurrencyPipe implements PipeTransform {
}

// @public
export const DATE_PIPE_DEFAULT_OPTIONS: InjectionToken<DatePipeConfig>;

// @public @deprecated
export const DATE_PIPE_DEFAULT_TIMEZONE: InjectionToken<string>;

// @public
export class DatePipe implements PipeTransform {
constructor(locale: string, defaultTimezone?: string | null | undefined);
constructor(locale: string, defaultTimezone?: string | null | undefined, defaultOptions?: DatePipeConfig | null | undefined);
// (undocumented)
transform(value: Date | string | number, format?: string, timezone?: string, locale?: string): string | null;
// (undocumented)
transform(value: null | undefined, format?: string, timezone?: string, locale?: string): null;
// (undocumented)
transform(value: Date | string | number | null | undefined, format?: string, timezone?: string, locale?: string): string | null;
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<DatePipe, [null, { optional: true; }]>;
static ɵfac: i0.ɵɵFactoryDeclaration<DatePipe, [null, { optional: true; }, { optional: true; }]>;
// (undocumented)
static ɵpipe: i0.ɵɵPipeDeclaration<DatePipe, "date", true>;
}

// @public
export interface DatePipeConfig {
// (undocumented)
dateFormat: string;
// (undocumented)
timezone: string;
}

// @public
export class DecimalPipe implements PipeTransform {
constructor(_locale: string);
Expand Down
2 changes: 1 addition & 1 deletion goldens/size-tracking/integration-payloads.json
Expand Up @@ -26,7 +26,7 @@
"cli-hello-world-ivy-i18n": {
"uncompressed": {
"runtime": 926,
"main": 124269,
"main": 124195,
"polyfills": 35252
}
},
Expand Down
2 changes: 1 addition & 1 deletion packages/common/src/common.ts
Expand Up @@ -22,7 +22,7 @@ export {parseCookieValue as ɵparseCookieValue} from './cookie';
export {CommonModule} from './common_module';
export {NgClass, NgFor, NgForOf, NgForOfContext, NgIf, NgIfContext, NgPlural, NgPluralCase, NgStyle, NgSwitch, NgSwitchCase, NgSwitchDefault, NgTemplateOutlet, NgComponentOutlet} from './directives/index';
export {DOCUMENT} from './dom_tokens';
export {AsyncPipe, DatePipe, DATE_PIPE_DEFAULT_TIMEZONE, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, UpperCasePipe, TitleCasePipe, KeyValuePipe, KeyValue} from './pipes/index';
export {AsyncPipe, DatePipe, DatePipeConfig, DATE_PIPE_DEFAULT_TIMEZONE, DATE_PIPE_DEFAULT_OPTIONS, I18nPluralPipe, I18nSelectPipe, JsonPipe, LowerCasePipe, CurrencyPipe, DecimalPipe, PercentPipe, SlicePipe, UpperCasePipe, TitleCasePipe, KeyValuePipe, KeyValue} from './pipes/index';
export {PLATFORM_BROWSER_ID as ɵPLATFORM_BROWSER_ID, PLATFORM_SERVER_ID as ɵPLATFORM_SERVER_ID, PLATFORM_WORKER_APP_ID as ɵPLATFORM_WORKER_APP_ID, PLATFORM_WORKER_UI_ID as ɵPLATFORM_WORKER_UI_ID, isPlatformBrowser, isPlatformServer, isPlatformWorkerApp, isPlatformWorkerUi} from './platform_id';
export {VERSION} from './version';
export {ViewportScroller, NullViewportScroller as ɵNullViewportScroller} from './viewport_scroller';
Expand Down
61 changes: 54 additions & 7 deletions packages/common/src/pipes/date_pipe.ts
Expand Up @@ -10,14 +10,51 @@ import {Inject, InjectionToken, LOCALE_ID, Optional, Pipe, PipeTransform} from '

import {formatDate} from '../i18n/format_date';

import {DatePipeConfig, DEFAULT_DATE_FORMAT} from './date_pipe_config';
import {invalidPipeArgumentError} from './invalid_pipe_argument_error';

/**
* Optionally-provided default timezone to use for all instances of `DatePipe` (such as `'+0430'`).
* If the value isn't provided, the `DatePipe` will use the end-user's local system timezone.
*
* @deprecated use DATE_PIPE_DEFAULT_OPTIONS token to configure DatePipe
*/
export const DATE_PIPE_DEFAULT_TIMEZONE = new InjectionToken<string>('DATE_PIPE_DEFAULT_TIMEZONE');

/**
* DI token that allows to provide default configuration for the `DatePipe` instances in an
* application. The value is an object which can include the following fields:
* - `dateFormat`: configures the default date format. If not provided, the `DatePipe`
* will use the 'mediumDate' as a value.
* - `timezone`: configures the default timezone. If not provided, the `DatePipe` will
* use the end-user's local system timezone.
*
* @see `DatePipeConfig`
*
* @usageNotes
*
* Various date pipe default values can be overwritten by providing this token with
* the value that has this interface.
*
* For example:
*
* Override the default date format by providing a value using the token:
* ```typescript
* providers: [
* {provide: DATE_PIPE_DEFAULT_OPTIONS, useValue: {dateFormat: 'shortDate'}}
* ]
* ```
*
* Override the default timezone by providing a value using the token:
* ```typescript
* providers: [
* {provide: DATE_PIPE_DEFAULT_OPTIONS, useValue: {timezone: '-1200'}}
* ]
* ```
*/
export const DATE_PIPE_DEFAULT_OPTIONS =
new InjectionToken<DatePipeConfig>('DATE_PIPE_DEFAULT_OPTIONS');

// clang-format off
/**
* @ngModule CommonModule
Expand Down Expand Up @@ -185,19 +222,27 @@ export const DATE_PIPE_DEFAULT_TIMEZONE = new InjectionToken<string>('DATE_PIPE_
export class DatePipe implements PipeTransform {
constructor(
@Inject(LOCALE_ID) private locale: string,
@Inject(DATE_PIPE_DEFAULT_TIMEZONE) @Optional() private defaultTimezone?: string|null) {}
@Inject(DATE_PIPE_DEFAULT_TIMEZONE) @Optional() private defaultTimezone?: string|null,
@Inject(DATE_PIPE_DEFAULT_OPTIONS) @Optional() private defaultOptions?: DatePipeConfig|null,
) {}

/**
* @param value The date expression: a `Date` object, a number
* (milliseconds since UTC epoch), or an ISO string (https://www.w3.org/TR/NOTE-datetime).
* @param format The date/time components to include, using predefined options or a
* custom format string.
* custom format string. When not provided, the `DatePipe` looks for the value using the
* `DATE_PIPE_DEFAULT_OPTIONS` injection token (and reads the `dateFormat` property).
* If the token is not configured, the `mediumDate` is used as a value.
* @param timezone A timezone offset (such as `'+0430'`), or a standard UTC/GMT, or continental US
* timezone abbreviation. When not supplied, either the value of the `DATE_PIPE_DEFAULT_TIMEZONE`
* injection token is used or the end-user's local system timezone.
* timezone abbreviation. When not provided, the `DatePipe` looks for the value using the
* `DATE_PIPE_DEFAULT_OPTIONS` injection token (and reads the `timezone` property). If the token
* is not configured, the end-user's local system timezone is used as a value.
* @param locale A locale code for the locale format rules to use.
* When not supplied, uses the value of `LOCALE_ID`, which is `en-US` by default.
* See [Setting your app locale](guide/i18n-common-locale-id).
*
* @see `DATE_PIPE_DEFAULT_OPTIONS`
*
* @returns A date string in the desired format.
*/
transform(value: Date|string|number, format?: string, timezone?: string, locale?: string): string
Expand All @@ -207,13 +252,15 @@ export class DatePipe implements PipeTransform {
value: Date|string|number|null|undefined, format?: string, timezone?: string,
locale?: string): string|null;
transform(
value: Date|string|number|null|undefined, format = 'mediumDate', timezone?: string,
value: Date|string|number|null|undefined, format?: string, timezone?: string,
locale?: string): string|null {
if (value == null || value === '' || value !== value) return null;

try {
return formatDate(
value, format, locale || this.locale, timezone ?? this.defaultTimezone ?? undefined);
const _format = format ?? this.defaultOptions?.dateFormat ?? DEFAULT_DATE_FORMAT;
const _timezone =
timezone ?? this.defaultOptions?.timezone ?? this.defaultTimezone ?? undefined;
return formatDate(value, _format, locale || this.locale, _timezone);
} catch (error) {
throw invalidPipeArgumentError(DatePipe, (error as Error).message);
}
Expand Down
26 changes: 26 additions & 0 deletions packages/common/src/pipes/date_pipe_config.ts
@@ -0,0 +1,26 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

/**
* An interface that describes the date pipe configuration, which can be provided using the
* `DATE_PIPE_DEFAULT_OPTIONS` token.
*
* @see `DATE_PIPE_DEFAULT_OPTIONS`
*
* @publicApi
*/
export interface DatePipeConfig {
dateFormat: string;
timezone: string;
}

/**
* The default date format of Angular date pipe, which corresponds to the following format:
* `'MMM d,y'` (e.g. `Jun 15, 2015`)
*/
export const DEFAULT_DATE_FORMAT = 'mediumDate';
5 changes: 4 additions & 1 deletion packages/common/src/pipes/index.ts
Expand Up @@ -13,7 +13,8 @@
*/
import {AsyncPipe} from './async_pipe';
import {LowerCasePipe, TitleCasePipe, UpperCasePipe} from './case_conversion_pipes';
import {DATE_PIPE_DEFAULT_TIMEZONE, DatePipe} from './date_pipe';
import {DATE_PIPE_DEFAULT_OPTIONS, DATE_PIPE_DEFAULT_TIMEZONE, DatePipe} from './date_pipe';
import {DatePipeConfig} from './date_pipe_config';
import {I18nPluralPipe} from './i18n_plural_pipe';
import {I18nSelectPipe} from './i18n_select_pipe';
import {JsonPipe} from './json_pipe';
Expand All @@ -24,8 +25,10 @@ import {SlicePipe} from './slice_pipe';
export {
AsyncPipe,
CurrencyPipe,
DATE_PIPE_DEFAULT_OPTIONS,
DATE_PIPE_DEFAULT_TIMEZONE,
DatePipe,
DatePipeConfig,
DecimalPipe,
I18nPluralPipe,
I18nSelectPipe,
Expand Down
93 changes: 91 additions & 2 deletions packages/common/test/pipes/date_pipe_spec.ts
Expand Up @@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {DatePipe} from '@angular/common';
import {DATE_PIPE_DEFAULT_OPTIONS, DatePipe} from '@angular/common';
import localeEn from '@angular/common/locales/en';
import localeEnExtra from '@angular/common/locales/extra/en';
import {Component, ɵregisterLocaleData, ɵunregisterLocaleData} from '@angular/core';
Expand Down Expand Up @@ -72,9 +72,55 @@ import {TestBed} from '@angular/core/testing';
});

describe('transform', () => {
it('should use "mediumDate" as the default format',
it('should use "mediumDate" as the default format if no format is provided',
() => expect(pipe.transform('2017-01-11T10:14:39+0000')).toEqual('Jan 11, 2017'));

it('should give precedence to the passed in format',
() => expect(pipe.transform('2017-01-11T10:14:39+0000', 'shortDate')).toEqual('1/11/17'));

it('should use format provided in component as default format when no format is passed in',
() => {
@Component({
selector: 'test-component',
imports: [DatePipe],
template: '{{ value | date }}',
standalone: true,
providers: [{provide: DATE_PIPE_DEFAULT_OPTIONS, useValue: {dateFormat: 'shortDate'}}]
})
class TestComponent {
value = '2017-01-11T10:14:39+0000';
}

const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();

const content = fixture.nativeElement.textContent;
expect(content).toBe('1/11/17');
});

it('should use format provided in module as default format when no format is passed in',
() => {
@Component({
selector: 'test-component',
imports: [DatePipe],
template: '{{ value | date }}',
standalone: true,
})
class TestComponent {
value = '2017-01-11T10:14:39+0000';
}

TestBed.configureTestingModule({
imports: [TestComponent],
providers: [{provide: DATE_PIPE_DEFAULT_OPTIONS, useValue: {dateFormat: 'shortDate'}}]
});
const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();

const content = fixture.nativeElement.textContent;
expect(content).toBe('1/11/17');
});

it('should return first week if some dates fall in previous year but belong to next year according to ISO 8601 format',
() => {
expect(pipe.transform('2019-12-28T00:00:00', 'w')).toEqual('52');
Expand Down Expand Up @@ -111,6 +157,49 @@ import {TestBed} from '@angular/core/testing';
expect(pipe.transform('2017-01-11T00:00:00', 'mediumDate', '+0100'))
.toEqual('Jan 11, 2017');
});

it('should use timezone provided in component as default timezone when no format is passed in',
() => {
@Component({
selector: 'test-component',
imports: [DatePipe],
template: '{{ value | date }}',
standalone: true,
providers: [{provide: DATE_PIPE_DEFAULT_OPTIONS, useValue: {timezone: '-1200'}}]
})
class TestComponent {
value = '2017-01-11T00:00:00';
}

const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();

const content = fixture.nativeElement.textContent;
expect(content).toBe('Jan 10, 2017');
});

it('should use timezone provided in module as default timezone when no format is passed in',
() => {
@Component({
selector: 'test-component',
imports: [DatePipe],
template: '{{ value | date }}',
standalone: true,
})
class TestComponent {
value = '2017-01-11T00:00:00';
}

TestBed.configureTestingModule({
imports: [TestComponent],
providers: [{provide: DATE_PIPE_DEFAULT_OPTIONS, useValue: {timezone: '-1200'}}]
});
const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();

const content = fixture.nativeElement.textContent;
expect(content).toBe('Jan 10, 2017');
});
});

it('should be available as a standalone pipe', () => {
Expand Down

0 comments on commit bdb5371

Please sign in to comment.