Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(common): add injection token for default DatePipe configuration #47157

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions aio/content/guide/deprecations.md
Expand Up @@ -85,6 +85,7 @@ v14 - v17
| `@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 @@ -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. |

<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).
matthiasweiss marked this conversation as resolved.
Show resolved Hide resolved
*
* @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 {
matthiasweiss marked this conversation as resolved.
Show resolved Hide resolved
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';
matthiasweiss marked this conversation as resolved.
Show resolved Hide resolved
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,
matthiasweiss marked this conversation as resolved.
Show resolved Hide resolved
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