diff --git a/aio/content/guide/deprecations.md b/aio/content/guide/deprecations.md index c62407cf5173bd..043e2e26b67c17 100644 --- a/aio/content/guide/deprecations.md +++ b/aio/content/guide/deprecations.md @@ -85,6 +85,7 @@ v14 - v17 | `@angular/service-worker` | [`SwUpdate#available`](api/service-worker/SwUpdate#available) | v16 | | template syntax | [`/deep/`, `>>>`, and `::ng-deep`](#deep-component-style-selector) | unspecified | | template syntax | [`bind-`, `on-`, `bindon-`, and `ref-`](#bind-syntax) | v15 | +| `@angular/common` | [`DatePipe` - `DATE_PIPE_DEFAULT_TIMEZONE`](api/common/DATE_PIPE_DEFAULT_TIMEZONE) | v17 | For information about Angular CDK and Angular Material deprecations, see the [changelog](https://github.com/angular/components/blob/main/CHANGELOG.md). @@ -107,6 +108,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. | diff --git a/goldens/public-api/common/index.md b/goldens/public-api/common/index.md index 106db1554959dd..368263a16a3943 100644 --- a/goldens/public-api/common/index.md +++ b/goldens/public-api/common/index.md @@ -76,11 +76,14 @@ export class CurrencyPipe implements PipeTransform { } // @public +export const DATE_PIPE_DEFAULT_OPTIONS: InjectionToken; + +// @public @deprecated export const DATE_PIPE_DEFAULT_TIMEZONE: InjectionToken; // @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) @@ -88,11 +91,19 @@ export class DatePipe implements PipeTransform { // (undocumented) transform(value: Date | string | number | null | undefined, format?: string, timezone?: string, locale?: string): string | null; // (undocumented) - static ɵfac: i0.ɵɵFactoryDeclaration; + static ɵfac: i0.ɵɵFactoryDeclaration; // (undocumented) static ɵpipe: i0.ɵɵPipeDeclaration; } +// @public +export interface DatePipeConfig { + // (undocumented) + dateFormat: string; + // (undocumented) + timezone: string; +} + // @public export class DecimalPipe implements PipeTransform { constructor(_locale: string); diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index dba601e85a20ef..18b8ab61afc8ee 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -26,7 +26,7 @@ "cli-hello-world-ivy-i18n": { "uncompressed": { "runtime": 926, - "main": 124269, + "main": 124779, "polyfills": 35252 } }, diff --git a/packages/common/src/common.ts b/packages/common/src/common.ts index 2bd0c424f8d865..1195385b42261e 100644 --- a/packages/common/src/common.ts +++ b/packages/common/src/common.ts @@ -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'; diff --git a/packages/common/src/pipes/date_pipe.ts b/packages/common/src/pipes/date_pipe.ts index aacb939eee39a4..e400f391b79a06 100644 --- a/packages/common/src/pipes/date_pipe.ts +++ b/packages/common/src/pipes/date_pipe.ts @@ -10,14 +10,52 @@ 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('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('DATE_PIPE_DEFAULT_OPTIONS'); + // clang-format off /** * @ngModule CommonModule @@ -185,19 +223,27 @@ export const DATE_PIPE_DEFAULT_TIMEZONE = new InjectionToken('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 @@ -207,13 +253,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); } diff --git a/packages/common/src/pipes/date_pipe_config.ts b/packages/common/src/pipes/date_pipe_config.ts new file mode 100644 index 00000000000000..90216b9c73e1b7 --- /dev/null +++ b/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'; diff --git a/packages/common/src/pipes/index.ts b/packages/common/src/pipes/index.ts index 54e6dedd6ccbfd..cd1b27307852f4 100644 --- a/packages/common/src/pipes/index.ts +++ b/packages/common/src/pipes/index.ts @@ -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'; @@ -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, diff --git a/packages/common/test/pipes/date_pipe_spec.ts b/packages/common/test/pipes/date_pipe_spec.ts index a047416580d86c..be3217dc181d05 100644 --- a/packages/common/test/pipes/date_pipe_spec.ts +++ b/packages/common/test/pipes/date_pipe_spec.ts @@ -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'; @@ -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'); @@ -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', () => {