Skip to content

Commit a07a66f

Browse files
authored
feat: add list pipe (#13)
1 parent 4ebd2da commit a07a66f

14 files changed

+233
-9
lines changed

Diff for: README.md

+26-8
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ The input date can be one of the following:
6464
* null
6565
* undefined
6666

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

7070
With the `INTL_DATE_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.
@@ -84,7 +84,7 @@ The input can be one of the following:
8484
* null
8585
* undefined
8686

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

9090
With the `INTL_DECIMAL_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.
@@ -104,7 +104,7 @@ The input can be one of the following:
104104
* null
105105
* undefined
106106

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

110110
With the `INTL_PERCENT_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.
@@ -127,7 +127,7 @@ The input can be one of the following:
127127
The currency code parameter is required and must be a valid ISO 4217 currency code. If you want to transform a decimal
128128
number instead, use the `intlDecimal` pipe.
129129

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

133133
With the `INTL_CURRENCY_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.
@@ -146,7 +146,7 @@ The input can be one of the following:
146146
* null
147147
* undefined
148148

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

152152
With the `INTL_LANGUAGE_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.
@@ -165,7 +165,7 @@ The input can be one of the following:
165165
* null
166166
* undefined
167167

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

171171
With the `INTL_COUNTRY_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.
@@ -189,18 +189,36 @@ The unit parameter is required, see
189189
the [specification](https://tc39.es/proposal-unified-intl-numberformat/section6/locales-currencies-tz_proposed_out.html#sec-issanctionedsimpleunitidentifier)
190190
for a full list of possible values. If you want to transform a decimal number instead, use the `intlDecimal` pipe.
191191

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

195195
With the `INTL_UNIT_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.
196196

197+
### List pipe
198+
199+
Use the list pipe like the following:
200+
201+
```
202+
{{['my', 'items'] | intlList: options}}
203+
```
204+
205+
The input can be one of the following:
206+
207+
* Iterable of strings
208+
* null
209+
* undefined
210+
211+
The options are a subset of the options for `new Intl.ListFormat()`. For a list of the options, see
212+
their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat#options).
213+
214+
With the `INTL_LIST_PIPE_DEFAULT_OPTIONS` injection token you can specify default options.
215+
197216
## Background
198217

199218
For more context, see the following [GitHub issue](https://github.com/angular/angular/issues/49143)
200219

201220
## Feature Roadmap
202221

203222
* Performance: Prepare Intl.* object with default options, only construct new object when necessary
204-
* List pipe
205223
* Relative time pipe
206224
* Migration Schematics for usages of Angular pipes

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

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {IntlPercentPipe} from "./percent/intl-percent.pipe";
66
import {IntlCurrencyPipe} from "./currency/intl-currency.pipe";
77
import {IntlCountryPipe} from "./country/intl-country.pipe";
88
import {IntlUnitPipe} from "./unit/intl-unit.pipe";
9+
import {IntlListPipe} from "./list/intl-list.pipe";
910

1011
@NgModule({
1112
imports: [
@@ -16,6 +17,7 @@ import {IntlUnitPipe} from "./unit/intl-unit.pipe";
1617
IntlCurrencyPipe,
1718
IntlCountryPipe,
1819
IntlUnitPipe,
20+
IntlListPipe,
1921
],
2022
exports: [
2123
IntlDatePipe,
@@ -25,6 +27,7 @@ import {IntlUnitPipe} from "./unit/intl-unit.pipe";
2527
IntlCurrencyPipe,
2628
IntlCountryPipe,
2729
IntlUnitPipe,
30+
IntlListPipe,
2831
],
2932
})
3033
export class IntlModule {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import {InjectionToken} from "@angular/core";
2+
import {IntlListPipeOptions} from "./intl-list.pipe";
3+
4+
export const INTL_LIST_PIPE_DEFAULT_OPTIONS = new InjectionToken<Omit<IntlListPipeOptions, 'locale'>>('IntlListPipeDefaultOptions');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import {IntlListPipe} from './intl-list.pipe';
2+
import {TestBed} from "@angular/core/testing";
3+
import {INTL_LOCALES} from "../locale";
4+
5+
describe('IntlListPipe', () => {
6+
let testUnit: IntlListPipe;
7+
8+
describe('parsing', () => {
9+
beforeEach(() => {
10+
testUnit = new IntlListPipe('en-US');
11+
});
12+
13+
it('should create an instance', () => {
14+
expect(testUnit).toBeTruthy();
15+
});
16+
17+
it('should handle null values', () => {
18+
expect(testUnit.transform(null)).toBeNull();
19+
});
20+
21+
it('should handle undefined values', () => {
22+
expect(testUnit.transform(undefined)).toBeNull();
23+
});
24+
25+
it('should handle empty arrays', () => {
26+
expect(testUnit.transform([])).toEqual('');
27+
});
28+
29+
it('should transform string arrays', () => {
30+
expect(testUnit.transform(['apples', 'pies'])).toEqual('apples and pies');
31+
});
32+
33+
it('should handle missing Intl.DisplayNames browser API', () => {
34+
// @ts-expect-error Intl APIs are not expected to be undefined
35+
spyOn(Intl, 'ListFormat').and.returnValue(undefined);
36+
const consoleError = spyOn(console, 'error');
37+
38+
expect(testUnit.transform(['some', 'val'])).toBeNull();
39+
expect(consoleError).toHaveBeenCalledTimes(1);
40+
});
41+
});
42+
43+
describe('internationalization', () => {
44+
it('should respect the set locale', () => {
45+
TestBed.configureTestingModule({
46+
providers: [
47+
IntlListPipe,
48+
{
49+
provide: INTL_LOCALES,
50+
useValue: 'de-DE',
51+
},
52+
],
53+
});
54+
testUnit = TestBed.inject(IntlListPipe);
55+
56+
expect(testUnit.transform(['Äpfel', 'Birnen'])).toEqual('Äpfel und Birnen');
57+
});
58+
59+
it('should fall back to the browser default locale', () => {
60+
TestBed.configureTestingModule({providers: [IntlListPipe]});
61+
62+
const result1 = TestBed.inject(IntlListPipe).transform(['some', 'val']);
63+
const result2 = new IntlListPipe(navigator.language).transform(['some', 'val']);
64+
65+
expect(result1).toEqual(result2);
66+
});
67+
});
68+
69+
it('should respect locale option', () => {
70+
TestBed.configureTestingModule({
71+
providers: [
72+
IntlListPipe,
73+
{
74+
provide: INTL_LOCALES,
75+
useValue: 'en-US',
76+
},
77+
],
78+
});
79+
testUnit = TestBed.inject(IntlListPipe);
80+
81+
expect(testUnit.transform(['Äpfel', 'Birnen'], {locale: 'de-DE'})).toEqual('Äpfel und Birnen');
82+
});
83+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import {Inject, Optional, Pipe, PipeTransform} from '@angular/core';
2+
import {INTL_LOCALES} from "../locale";
3+
import {INTL_LIST_PIPE_DEFAULT_OPTIONS} from "./intl-list-pipe-default-options";
4+
import {IntlPipeOptions} from "../intl-pipe-options";
5+
6+
export type IntlListPipeOptions = Partial<Intl.ListFormatOptions> & IntlPipeOptions;
7+
8+
@Pipe({
9+
name: 'intlList',
10+
standalone: true,
11+
})
12+
export class IntlListPipe implements PipeTransform {
13+
14+
constructor(@Optional() @Inject(INTL_LOCALES) readonly locale?: string | string[] | null,
15+
@Optional() @Inject(INTL_LIST_PIPE_DEFAULT_OPTIONS) readonly defaultOptions?: Omit<IntlListPipeOptions, 'locale'> | null) {
16+
}
17+
18+
transform(value: Iterable<string> | null | undefined, options?: IntlListPipeOptions): string | null {
19+
if (!value) {
20+
return null;
21+
}
22+
23+
const {locale, ...intlOptions} = options ?? {};
24+
25+
try {
26+
return new Intl.ListFormat(locale ?? this.locale ?? undefined, {
27+
...this.defaultOptions, ...intlOptions,
28+
}).format(value);
29+
} catch (e) {
30+
console.error('Error while transforming the list', e);
31+
return null;
32+
}
33+
}
34+
35+
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class IntlUnitPipe implements PipeTransform {
3232
{...this.defaultOptions, ...intlOptions, unit, style: 'unit'},
3333
).format(numericValue);
3434
} catch (e) {
35-
console.error('Error while transforming the percent value', e);
35+
console.error('Error while transforming the unit value', e);
3636
return null;
3737
}
3838
}

Diff for: projects/angular-ecmascript-intl/src/public-api.ts

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export * from './lib/decimal/intl-decimal-pipe-default-options';
1313
export * from './lib/intl.module';
1414
export * from './lib/language/intl-language.pipe';
1515
export * from './lib/language/intl-language-pipe-default-options';
16+
export * from './lib/list/intl-list.pipe';
17+
export * from './lib/list/intl-list-pipe-default-options';
1618
export * from './lib/locale';
1719
export * from './lib/percent/intl-percent.pipe';
1820
export * from './lib/percent/intl-percent-pipe-default-options';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<div class="fields-container">
2+
<mat-form-field>
3+
<mat-label>List items</mat-label>
4+
<mat-select [(ngModel)]="selectedItems" multiple>
5+
<mat-option *ngFor="let item of list" [value]="item">{{item}}</mat-option>
6+
</mat-select>
7+
</mat-form-field>
8+
9+
<mat-form-field>
10+
<mat-label>Locale</mat-label>
11+
<mat-select [(ngModel)]="locale">
12+
<mat-option [value]="undefined">Browser default</mat-option>
13+
<mat-option *ngFor="let language of languages" [value]="language">{{language}}</mat-option>
14+
</mat-select>
15+
</mat-form-field>
16+
17+
<mat-form-field>
18+
<mat-label>Type</mat-label>
19+
<mat-select [(ngModel)]="type">
20+
<mat-option [value]="undefined">Browser default</mat-option>
21+
<mat-option [value]="'conjunction'">conjunction</mat-option>
22+
<mat-option [value]="'disjunction'">disjunction</mat-option>
23+
<mat-option [value]="'unit'">unit</mat-option>
24+
</mat-select>
25+
</mat-form-field>
26+
27+
<mat-form-field>
28+
<mat-label>Style</mat-label>
29+
<mat-select [(ngModel)]="style">
30+
<mat-option [value]="undefined">Browser default</mat-option>
31+
<mat-option [value]="'long'">long</mat-option>
32+
<mat-option [value]="'short'">short</mat-option>
33+
<mat-option [value]="'narrow'">narrow</mat-option>
34+
</mat-select>
35+
</mat-form-field>
36+
</div>
37+
38+
<p>{{selectedItems | intlList: {locale, type, style} }}</p>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.fields-container {
2+
display: flex;
3+
gap: 16px;
4+
flex-wrap: wrap;
5+
align-items: center;
6+
margin-bottom: 16px;
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import {Component} from '@angular/core';
2+
import {languages} from "../../languages";
3+
import {list} from "./list";
4+
import {IntlListPipeOptions} from "projects/angular-ecmascript-intl/src/lib/list/intl-list.pipe";
5+
6+
@Component({
7+
selector: 'app-list',
8+
templateUrl: './list.component.html',
9+
styleUrls: ['./list.component.scss'],
10+
})
11+
export class ListComponent {
12+
languages = languages;
13+
list = list;
14+
selectedItems: string[] = [list[0], list[2], list[3]];
15+
locale?: string;
16+
type?: IntlListPipeOptions['type'];
17+
style?: IntlListPipeOptions['style'];
18+
}
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const list = [
2+
'Pizza',
3+
'Lasagne',
4+
'Gnocchi',
5+
'Spaghetti',
6+
'Pesto',
7+
];

Diff for: projects/angular-intl-demo/src/app/pipes/pipes-routing.module.ts

+5
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {CurrencyComponent} from "./currency/currency.component";
88
import {LanguageComponent} from "./language/language.component";
99
import {CountryComponent} from "./country/country.component";
1010
import {UnitComponent} from "./unit/unit.component";
11+
import {ListComponent} from "./list/list.component";
1112

1213
const routes: Routes = [
1314
{
@@ -42,6 +43,10 @@ const routes: Routes = [
4243
path: 'country',
4344
component: CountryComponent,
4445
},
46+
{
47+
path: 'list',
48+
component: ListComponent,
49+
},
4550
{
4651
path: '',
4752
redirectTo: 'date',

Diff for: projects/angular-intl-demo/src/app/pipes/pipes.component.html

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
routerLinkActive>Language</a>
1414
<a #countryActive="routerLinkActive" [active]="countryActive.isActive" mat-tab-link routerLink="country"
1515
routerLinkActive>Country</a>
16+
<a #listActive="routerLinkActive" [active]="listActive.isActive" mat-tab-link routerLink="list"
17+
routerLinkActive>List</a>
1618
</nav>
1719
<mat-tab-nav-panel #tabPanel></mat-tab-nav-panel>
1820

Diff for: projects/angular-intl-demo/src/app/pipes/pipes.module.ts

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {CountryComponent} from "./country/country.component";
1616
import {NgxMatDatetimePickerModule, NgxMatNativeDateModule} from "@angular-material-components/datetime-picker";
1717
import {MatDatepickerModule} from "@angular/material/datepicker";
1818
import {UnitComponent} from "./unit/unit.component";
19+
import {ListComponent} from "./list/list.component";
1920

2021
@NgModule({
2122
declarations: [
@@ -27,6 +28,7 @@ import {UnitComponent} from "./unit/unit.component";
2728
PipesComponent,
2829
CountryComponent,
2930
UnitComponent,
31+
ListComponent,
3032
],
3133
imports: [
3234
CommonModule,

0 commit comments

Comments
 (0)