Skip to content

Commit b6b5aec

Browse files
committed
feat: add number pipes
1 parent 3dd0d32 commit b6b5aec

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+1351
-631
lines changed

Diff for: CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
# Changelog
22

3-
## Unreleased
3+
## 0.2.0
44

55
### Added
66

7+
* Add `intlDecimal` pipe
8+
* Add `intlPercent` pipe
9+
* Add `intlCurrency` pipe
710
* Add option to override locale
811

912
## 0.1.0 - 2023-02-19

Diff for: README.md

+65-9
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,69 @@ their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/G
6969

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

72+
### Decimal pipe
73+
74+
Use the decimal pipe like the following:
75+
76+
```
77+
{{1.24 | intlDecimal: options}}
78+
```
79+
80+
The input can be one of the following:
81+
82+
* number
83+
* string (must be parseable as number)
84+
* null
85+
* undefined
86+
87+
The options are the same as the options for `new Intl.NumberFormat()`. For a list of the options, see
88+
their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat).
89+
90+
With the `INTL_DECIMAL_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.
91+
92+
### Percent pipe
93+
94+
Use the percent pipe like the following:
95+
96+
```
97+
{{0.24 | intlPercent: options}}
98+
```
99+
100+
The input can be one of the following:
101+
102+
* number
103+
* string (must be parseable as number)
104+
* null
105+
* undefined
106+
107+
The options are the same as the options for `new Intl.NumberFormat()`. For a list of the options, see
108+
their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat).
109+
110+
With the `INTL_PERCENT_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.
111+
112+
### Currency pipe
113+
114+
Use the currency pipe like the following:
115+
116+
```
117+
{{1.24 | intlCurrency: 'USD': options}}
118+
```
119+
120+
The input can be one of the following:
121+
122+
* number
123+
* string (must be parseable as number)
124+
* null
125+
* undefined
126+
127+
The currency code parameter is required and must be a valid ISO 4217 currency code. If you want to transform a decimal
128+
number instead, use the `intlDecimal` pipe.
129+
130+
The options are the same as the options for `new Intl.NumberFormat()`. For a list of the options, see
131+
their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat).
132+
133+
With the `INTL_CURRENCY_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.
134+
72135
### Language pipe
73136

74137
Use the language pipe like the following:
@@ -77,7 +140,7 @@ Use the language pipe like the following:
77140
{{'en-US' | intlLanguage: options}}
78141
```
79142

80-
The input date can be one of the following:
143+
The input can be one of the following:
81144

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

91154
## Background
92155

93-
Working with Angular's built-in pipes which support internationalization works fine when only supporting one locale.
94-
But nowadays, you want to support many locales, to give every user a good user experience. To get this working with
95-
Angular's built-in pipes can be time-consuming, because data for every locale must be included
96-
to the application. This increases bundle size and load times.
97-
98-
Modern browsers are fully capable of handling internationalization with the `Intl.*` browser APIs. There is no need for
99-
loading any locale date. This package re-implements some Angular built-in pipes such as `date` using these APIs.
156+
For more context, see the following [GitHub issue](https://github.com/angular/angular/issues/49143)
100157

101158
## Feature Roadmap
102159

103-
* Number pipe(s): decimal, currency, percentage
104160
* Performance: Prepare Intl.* object with default options, only construct new object when necessary
105161
* Country pipe
106162
* Relative time

Diff for: angular.json

+4-1
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,13 @@
6969
],
7070
"styles": [
7171
"@angular/material/prebuilt-themes/deeppurple-amber.css",
72+
"node_modules/prismjs/themes/prism-okaidia.css",
7273
"projects/angular-intl-demo/src/styles.scss"
7374
],
7475
"scripts": [
75-
"node_modules/marked/marked.min.js"
76+
"node_modules/marked/marked.min.js",
77+
"node_modules/prismjs/prism.js",
78+
"node_modules/prismjs/components/prism-typescript.min.js"
7679
]
7780
},
7881
"configurations": {

Diff for: package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: package.json

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"@angular/router": "^15.1.0",
2525
"marked": "^4.2.12",
2626
"ngx-markdown": "^15.1.1",
27+
"prismjs": "^1.29.0",
2728
"rxjs": "~7.8.0",
2829
"tslib": "^2.3.0",
2930
"zone.js": "~0.12.0"

Diff for: projects/angular-ecmascript-intl/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "angular-ecmascript-intl",
3-
"version": "0.1.0",
3+
"version": "0.2.0",
44
"peerDependencies": {
55
"@angular/common": "^15.1.0",
66
"@angular/core": "^15.1.0"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import {InjectionToken} from "@angular/core";
2+
3+
export const INTL_CURRENCY_PIPE_DEFAULT_OPTIONS = new InjectionToken<Partial<Intl.NumberFormatOptions>>('IntlCurrencyPipeDefaultOptions');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import {IntlCurrencyPipe} from './intl-currency.pipe';
2+
import {TestBed} from "@angular/core/testing";
3+
import {INTL_LOCALES} from "../locale";
4+
import {INTL_CURRENCY_PIPE_DEFAULT_OPTIONS} from "./intl-currency-pipe-default-options";
5+
6+
describe('IntlCurrencyPipe', () => {
7+
let testUnit: IntlCurrencyPipe;
8+
9+
describe('parsing', () => {
10+
beforeEach(() => {
11+
testUnit = new IntlCurrencyPipe('en-US');
12+
});
13+
14+
it('should create an instance', () => {
15+
expect(testUnit).toBeTruthy();
16+
});
17+
18+
it('should handle null values', () => {
19+
expect(testUnit.transform(null, 'USD')).toBeNull();
20+
});
21+
22+
it('should handle undefined values', () => {
23+
expect(testUnit.transform(undefined, 'USD')).toBeNull();
24+
});
25+
26+
it('should handle empty strings', () => {
27+
expect(testUnit.transform('', 'USD')).toBeNull();
28+
});
29+
30+
it('should transform numbers', () => {
31+
expect(testUnit.transform(1024.224, 'USD')).toEqual('$1,024.22');
32+
});
33+
34+
it('should transform strings', () => {
35+
expect(testUnit.transform('1024.224', 'USD')).toEqual('$1,024.22');
36+
});
37+
38+
it('should handle invalid strings', () => {
39+
expect(() => testUnit.transform('invalid number', 'USD')).toThrow();
40+
});
41+
42+
it('should handle missing Intl.NumberFormat browser API', () => {
43+
// @ts-expect-error
44+
spyOn(Intl, 'NumberFormat').and.returnValue(undefined);
45+
const consoleError = spyOn(console, 'error');
46+
expect(testUnit.transform('1', 'USD')).toBeNull();
47+
48+
expect(consoleError).toHaveBeenCalledTimes(1);
49+
});
50+
});
51+
52+
describe('internationalization', () => {
53+
it('should respect the set locale', () => {
54+
TestBed.configureTestingModule({
55+
providers: [
56+
IntlCurrencyPipe,
57+
{
58+
provide: INTL_LOCALES,
59+
useValue: 'de-DE',
60+
},
61+
],
62+
});
63+
testUnit = TestBed.inject(IntlCurrencyPipe);
64+
65+
expect(testUnit.transform(1024.2249, 'EUR')).toEqual('1.024,22\xa0€');
66+
});
67+
68+
it('should fall back to the browser default locale', () => {
69+
TestBed.configureTestingModule({providers: [IntlCurrencyPipe]});
70+
71+
const result1 = TestBed.inject(IntlCurrencyPipe).transform(1024.2249, 'EUR');
72+
const result2 = new IntlCurrencyPipe(navigator.language).transform(1024.2249, 'EUR');
73+
74+
expect(result1).toEqual(result2);
75+
});
76+
});
77+
78+
describe('options', () => {
79+
it('should respect the setting from default config', () => {
80+
TestBed.configureTestingModule({
81+
providers: [
82+
IntlCurrencyPipe,
83+
{
84+
provide: INTL_LOCALES,
85+
useValue: 'en-US',
86+
},
87+
{
88+
provide: INTL_CURRENCY_PIPE_DEFAULT_OPTIONS,
89+
useValue: {
90+
signDisplay: 'always',
91+
},
92+
},
93+
],
94+
});
95+
testUnit = TestBed.inject(IntlCurrencyPipe);
96+
97+
expect(testUnit.transform(1, 'USD')).toEqual('+$1.00');
98+
99+
});
100+
101+
it('should give the user options a higher priority', () => {
102+
TestBed.configureTestingModule({
103+
providers: [
104+
IntlCurrencyPipe,
105+
{
106+
provide: INTL_LOCALES,
107+
useValue: 'en-US',
108+
},
109+
{
110+
provide: INTL_CURRENCY_PIPE_DEFAULT_OPTIONS,
111+
useValue: {
112+
signDisplay: 'exceptZero',
113+
},
114+
},
115+
],
116+
});
117+
testUnit = TestBed.inject(IntlCurrencyPipe);
118+
119+
expect(testUnit.transform(1, 'USD', {signDisplay: 'never'})).toEqual('$1.00');
120+
});
121+
});
122+
123+
it('should respect locale option', () => {
124+
TestBed.configureTestingModule({
125+
providers: [
126+
IntlCurrencyPipe,
127+
{
128+
provide: INTL_LOCALES,
129+
useValue: 'en-US',
130+
},
131+
],
132+
});
133+
testUnit = TestBed.inject(IntlCurrencyPipe);
134+
135+
expect(testUnit.transform(1024, 'USD', {locale: 'de-DE'})).toEqual('1.024,00\xa0$');
136+
});
137+
138+
it('should not override the style option', () => {
139+
TestBed.configureTestingModule({
140+
providers: [
141+
IntlCurrencyPipe,
142+
{
143+
provide: INTL_LOCALES,
144+
useValue: 'en-US',
145+
},
146+
{
147+
provide: INTL_CURRENCY_PIPE_DEFAULT_OPTIONS,
148+
useValue: {
149+
style: 'percent',
150+
},
151+
},
152+
],
153+
});
154+
testUnit = TestBed.inject(IntlCurrencyPipe);
155+
156+
expect(testUnit.transform(1, 'USD', {style: 'percent'})).toEqual('$1.00');
157+
});
158+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import {Inject, Optional, Pipe, PipeTransform} from '@angular/core';
2+
import {IntlPipeOptions} from "../intl-pipe-options";
3+
import {INTL_LOCALES} from "../locale";
4+
import {INTL_CURRENCY_PIPE_DEFAULT_OPTIONS} from "./intl-currency-pipe-default-options";
5+
import {getNumericValue} from "../utils/number-utils";
6+
7+
export type IntlCurrencyPipeOptions = Partial<Intl.NumberFormatOptions> & IntlPipeOptions;
8+
9+
@Pipe({
10+
name: 'intlCurrency',
11+
standalone: true,
12+
})
13+
export class IntlCurrencyPipe implements PipeTransform {
14+
15+
constructor(@Optional() @Inject(INTL_LOCALES) readonly locale?: string | string[] | null,
16+
@Optional() @Inject(INTL_CURRENCY_PIPE_DEFAULT_OPTIONS) readonly defaultOptions?: Partial<Intl.NumberFormatOptions> | null) {
17+
}
18+
19+
transform(value: number | string | null | undefined, currency: string, options?: IntlCurrencyPipeOptions): string | null {
20+
if (typeof value !== 'number' && !value) {
21+
return null;
22+
}
23+
24+
const numericValue = getNumericValue(value);
25+
26+
const {locale, ...intlOptions} = options ?? {};
27+
28+
try {
29+
return new Intl.NumberFormat(
30+
locale ?? this.locale ?? undefined,
31+
{...this.defaultOptions, ...intlOptions, currency, style: 'currency'},
32+
).format(numericValue);
33+
} catch (e) {
34+
console.error('Error while transforming the currency', e);
35+
return null;
36+
}
37+
}
38+
39+
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { InjectionToken } from "@angular/core";
2-
import { IntlDatePipeOptions } from "./intl-date.pipe";
1+
import {InjectionToken} from "@angular/core";
32

4-
export const INTL_DATE_PIPE_DEFAULT_OPTIONS = new InjectionToken<IntlDatePipeOptions>('IntlDatePipeDefaultOptions');
3+
export const INTL_DATE_PIPE_DEFAULT_OPTIONS = new InjectionToken<Partial<Intl.DateTimeFormatOptions>>('IntlDatePipeDefaultOptions');

Diff for: projects/angular-ecmascript-intl/src/lib/date/intl-date.pipe.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export type IntlDatePipeOptions = Partial<Intl.DateTimeFormatOptions> & IntlPipe
1212
export class IntlDatePipe implements PipeTransform {
1313

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

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

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import {InjectionToken} from "@angular/core";
2+
3+
export const INTL_DECIMAL_PIPE_DEFAULT_OPTIONS = new InjectionToken<Partial<Intl.NumberFormatOptions>>('IntlDecimalPipeDefaultOptions');

0 commit comments

Comments
 (0)