Skip to content

Commit

Permalink
feat: add number pipes
Browse files Browse the repository at this point in the history
  • Loading branch information
json-derulo committed Feb 20, 2023
1 parent 3dd0d32 commit b6b5aec
Show file tree
Hide file tree
Showing 59 changed files with 1,351 additions and 631 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
# Changelog

## Unreleased
## 0.2.0

### Added

* Add `intlDecimal` pipe
* Add `intlPercent` pipe
* Add `intlCurrency` pipe
* Add option to override locale

## 0.1.0 - 2023-02-19
Expand Down
74 changes: 65 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,69 @@ their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/G

With the `INTL_DATE_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.

### Decimal pipe

Use the decimal pipe like the following:

```
{{1.24 | intlDecimal: options}}
```

The input can be one of the following:

* number
* string (must be parseable as number)
* null
* undefined

The options are the same as the options for `new Intl.NumberFormat()`. For a list of the options, see
their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat).

With the `INTL_DECIMAL_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.

### Percent pipe

Use the percent pipe like the following:

```
{{0.24 | intlPercent: options}}
```

The input can be one of the following:

* number
* string (must be parseable as number)
* null
* undefined

The options are the same as the options for `new Intl.NumberFormat()`. For a list of the options, see
their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat).

With the `INTL_PERCENT_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.

### Currency pipe

Use the currency pipe like the following:

```
{{1.24 | intlCurrency: 'USD': options}}
```

The input can be one of the following:

* number
* string (must be parseable as number)
* null
* undefined

The currency code parameter is required and must be a valid ISO 4217 currency code. If you want to transform a decimal
number instead, use the `intlDecimal` pipe.

The options are the same as the options for `new Intl.NumberFormat()`. For a list of the options, see
their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat).

With the `INTL_CURRENCY_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.

### Language pipe

Use the language pipe like the following:
Expand All @@ -77,7 +140,7 @@ Use the language pipe like the following:
{{'en-US' | intlLanguage: options}}
```

The input date can be one of the following:
The input can be one of the following:

* string (must be a BCP 47 IETF language tag)
* null
Expand All @@ -90,17 +153,10 @@ With the `INTL_LANGUAGE_PIPE_DEFAULT_OPTIONS` injection token you can specify de

## Background

Working with Angular's built-in pipes which support internationalization works fine when only supporting one locale.
But nowadays, you want to support many locales, to give every user a good user experience. To get this working with
Angular's built-in pipes can be time-consuming, because data for every locale must be included
to the application. This increases bundle size and load times.

Modern browsers are fully capable of handling internationalization with the `Intl.*` browser APIs. There is no need for
loading any locale date. This package re-implements some Angular built-in pipes such as `date` using these APIs.
For more context, see the following [GitHub issue](https://github.com/angular/angular/issues/49143)

## Feature Roadmap

* Number pipe(s): decimal, currency, percentage
* Performance: Prepare Intl.* object with default options, only construct new object when necessary
* Country pipe
* Relative time
Expand Down
5 changes: 4 additions & 1 deletion angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,13 @@
],
"styles": [
"@angular/material/prebuilt-themes/deeppurple-amber.css",
"node_modules/prismjs/themes/prism-okaidia.css",
"projects/angular-intl-demo/src/styles.scss"
],
"scripts": [
"node_modules/marked/marked.min.js"
"node_modules/marked/marked.min.js",
"node_modules/prismjs/prism.js",
"node_modules/prismjs/components/prism-typescript.min.js"
]
},
"configurations": {
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@angular/router": "^15.1.0",
"marked": "^4.2.12",
"ngx-markdown": "^15.1.1",
"prismjs": "^1.29.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "~0.12.0"
Expand Down
2 changes: 1 addition & 1 deletion projects/angular-ecmascript-intl/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "angular-ecmascript-intl",
"version": "0.1.0",
"version": "0.2.0",
"peerDependencies": {
"@angular/common": "^15.1.0",
"@angular/core": "^15.1.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {InjectionToken} from "@angular/core";

export const INTL_CURRENCY_PIPE_DEFAULT_OPTIONS = new InjectionToken<Partial<Intl.NumberFormatOptions>>('IntlCurrencyPipeDefaultOptions');
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import {IntlCurrencyPipe} from './intl-currency.pipe';
import {TestBed} from "@angular/core/testing";
import {INTL_LOCALES} from "../locale";
import {INTL_CURRENCY_PIPE_DEFAULT_OPTIONS} from "./intl-currency-pipe-default-options";

describe('IntlCurrencyPipe', () => {
let testUnit: IntlCurrencyPipe;

describe('parsing', () => {
beforeEach(() => {
testUnit = new IntlCurrencyPipe('en-US');
});

it('should create an instance', () => {
expect(testUnit).toBeTruthy();
});

it('should handle null values', () => {
expect(testUnit.transform(null, 'USD')).toBeNull();
});

it('should handle undefined values', () => {
expect(testUnit.transform(undefined, 'USD')).toBeNull();
});

it('should handle empty strings', () => {
expect(testUnit.transform('', 'USD')).toBeNull();
});

it('should transform numbers', () => {
expect(testUnit.transform(1024.224, 'USD')).toEqual('$1,024.22');
});

it('should transform strings', () => {
expect(testUnit.transform('1024.224', 'USD')).toEqual('$1,024.22');
});

it('should handle invalid strings', () => {
expect(() => testUnit.transform('invalid number', 'USD')).toThrow();
});

it('should handle missing Intl.NumberFormat browser API', () => {
// @ts-expect-error
spyOn(Intl, 'NumberFormat').and.returnValue(undefined);
const consoleError = spyOn(console, 'error');
expect(testUnit.transform('1', 'USD')).toBeNull();

expect(consoleError).toHaveBeenCalledTimes(1);
});
});

describe('internationalization', () => {
it('should respect the set locale', () => {
TestBed.configureTestingModule({
providers: [
IntlCurrencyPipe,
{
provide: INTL_LOCALES,
useValue: 'de-DE',
},
],
});
testUnit = TestBed.inject(IntlCurrencyPipe);

expect(testUnit.transform(1024.2249, 'EUR')).toEqual('1.024,22\xa0€');
});

it('should fall back to the browser default locale', () => {
TestBed.configureTestingModule({providers: [IntlCurrencyPipe]});

const result1 = TestBed.inject(IntlCurrencyPipe).transform(1024.2249, 'EUR');
const result2 = new IntlCurrencyPipe(navigator.language).transform(1024.2249, 'EUR');

expect(result1).toEqual(result2);
});
});

describe('options', () => {
it('should respect the setting from default config', () => {
TestBed.configureTestingModule({
providers: [
IntlCurrencyPipe,
{
provide: INTL_LOCALES,
useValue: 'en-US',
},
{
provide: INTL_CURRENCY_PIPE_DEFAULT_OPTIONS,
useValue: {
signDisplay: 'always',
},
},
],
});
testUnit = TestBed.inject(IntlCurrencyPipe);

expect(testUnit.transform(1, 'USD')).toEqual('+$1.00');

});

it('should give the user options a higher priority', () => {
TestBed.configureTestingModule({
providers: [
IntlCurrencyPipe,
{
provide: INTL_LOCALES,
useValue: 'en-US',
},
{
provide: INTL_CURRENCY_PIPE_DEFAULT_OPTIONS,
useValue: {
signDisplay: 'exceptZero',
},
},
],
});
testUnit = TestBed.inject(IntlCurrencyPipe);

expect(testUnit.transform(1, 'USD', {signDisplay: 'never'})).toEqual('$1.00');
});
});

it('should respect locale option', () => {
TestBed.configureTestingModule({
providers: [
IntlCurrencyPipe,
{
provide: INTL_LOCALES,
useValue: 'en-US',
},
],
});
testUnit = TestBed.inject(IntlCurrencyPipe);

expect(testUnit.transform(1024, 'USD', {locale: 'de-DE'})).toEqual('1.024,00\xa0$');
});

it('should not override the style option', () => {
TestBed.configureTestingModule({
providers: [
IntlCurrencyPipe,
{
provide: INTL_LOCALES,
useValue: 'en-US',
},
{
provide: INTL_CURRENCY_PIPE_DEFAULT_OPTIONS,
useValue: {
style: 'percent',
},
},
],
});
testUnit = TestBed.inject(IntlCurrencyPipe);

expect(testUnit.transform(1, 'USD', {style: 'percent'})).toEqual('$1.00');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {Inject, Optional, Pipe, PipeTransform} from '@angular/core';
import {IntlPipeOptions} from "../intl-pipe-options";
import {INTL_LOCALES} from "../locale";
import {INTL_CURRENCY_PIPE_DEFAULT_OPTIONS} from "./intl-currency-pipe-default-options";
import {getNumericValue} from "../utils/number-utils";

export type IntlCurrencyPipeOptions = Partial<Intl.NumberFormatOptions> & IntlPipeOptions;

@Pipe({
name: 'intlCurrency',
standalone: true,
})
export class IntlCurrencyPipe implements PipeTransform {

constructor(@Optional() @Inject(INTL_LOCALES) readonly locale?: string | string[] | null,
@Optional() @Inject(INTL_CURRENCY_PIPE_DEFAULT_OPTIONS) readonly defaultOptions?: Partial<Intl.NumberFormatOptions> | null) {
}

transform(value: number | string | null | undefined, currency: string, options?: IntlCurrencyPipeOptions): string | null {
if (typeof value !== 'number' && !value) {
return null;
}

const numericValue = getNumericValue(value);

const {locale, ...intlOptions} = options ?? {};

try {
return new Intl.NumberFormat(
locale ?? this.locale ?? undefined,
{...this.defaultOptions, ...intlOptions, currency, style: 'currency'},
).format(numericValue);
} catch (e) {
console.error('Error while transforming the currency', e);
return null;
}
}

}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { InjectionToken } from "@angular/core";
import { IntlDatePipeOptions } from "./intl-date.pipe";
import {InjectionToken} from "@angular/core";

export const INTL_DATE_PIPE_DEFAULT_OPTIONS = new InjectionToken<IntlDatePipeOptions>('IntlDatePipeDefaultOptions');
export const INTL_DATE_PIPE_DEFAULT_OPTIONS = new InjectionToken<Partial<Intl.DateTimeFormatOptions>>('IntlDatePipeDefaultOptions');
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export type IntlDatePipeOptions = Partial<Intl.DateTimeFormatOptions> & IntlPipe
export class IntlDatePipe implements PipeTransform {

constructor(@Optional() @Inject(INTL_LOCALES) readonly locale?: string | string[] | null,
@Optional() @Inject(INTL_DATE_PIPE_DEFAULT_OPTIONS) readonly defaultOptions?: IntlDatePipeOptions | null) {
@Optional() @Inject(INTL_DATE_PIPE_DEFAULT_OPTIONS) readonly defaultOptions?: Partial<Intl.DateTimeFormatOptions> | null) {
}

transform(value: string | number | Date | null | undefined, options?: IntlDatePipeOptions): string | null {
Expand All @@ -31,7 +31,7 @@ export class IntlDatePipe implements PipeTransform {
return new Intl.DateTimeFormat(locale ?? this.locale ?? undefined, {...this.defaultOptions, ...intlOptions}).format(date);
} catch (e) {
console.error('Error while transforming the date', e);
return date.toString();
return null;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {InjectionToken} from "@angular/core";

export const INTL_DECIMAL_PIPE_DEFAULT_OPTIONS = new InjectionToken<Partial<Intl.NumberFormatOptions>>('IntlDecimalPipeDefaultOptions');
Loading

0 comments on commit b6b5aec

Please sign in to comment.