From 37e2bdf3f779ae8d89e8615ae74eff7bf3c6b331 Mon Sep 17 00:00:00 2001 From: Daniel Kimmich Date: Sat, 25 Feb 2023 11:55:48 +0100 Subject: [PATCH] feat: add list pipe --- README.md | 34 ++++++-- .../src/lib/intl.module.ts | 3 + .../list/intl-list-pipe-default-options.ts | 4 + .../src/lib/list/intl-list.pipe.spec.ts | 83 +++++++++++++++++++ .../src/lib/list/intl-list.pipe.ts | 35 ++++++++ .../src/lib/unit/intl-unit.pipe.ts | 2 +- .../angular-ecmascript-intl/src/public-api.ts | 2 + .../src/app/pipes/list/list.component.html | 38 +++++++++ .../src/app/pipes/list/list.component.scss | 7 ++ .../src/app/pipes/list/list.component.ts | 18 ++++ .../src/app/pipes/list/list.ts | 7 ++ .../src/app/pipes/pipes-routing.module.ts | 5 ++ .../src/app/pipes/pipes.component.html | 2 + .../src/app/pipes/pipes.module.ts | 2 + 14 files changed, 233 insertions(+), 9 deletions(-) create mode 100644 projects/angular-ecmascript-intl/src/lib/list/intl-list-pipe-default-options.ts create mode 100644 projects/angular-ecmascript-intl/src/lib/list/intl-list.pipe.spec.ts create mode 100644 projects/angular-ecmascript-intl/src/lib/list/intl-list.pipe.ts create mode 100644 projects/angular-intl-demo/src/app/pipes/list/list.component.html create mode 100644 projects/angular-intl-demo/src/app/pipes/list/list.component.scss create mode 100644 projects/angular-intl-demo/src/app/pipes/list/list.component.ts create mode 100644 projects/angular-intl-demo/src/app/pipes/list/list.ts diff --git a/README.md b/README.md index b45dc12a..c7650e8c 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ The input date can be one of the following: * null * undefined -The options are the same as the options for `new Intl.DateTimeFormat()`. For a list of the options, see +The options are a subset of the options for `new Intl.DateTimeFormat()`. For a list of the options, see their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/DateTimeFormat#options). 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: * null * undefined -The options are the same as the options for `new Intl.NumberFormat()`. For a list of the options, see +The options are a subset of 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#options). 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: * null * undefined -The options are the same as the options for `new Intl.NumberFormat()`. For a list of the options, see +The options are a subset of 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#options). 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: 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 +The options are a subset of 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#options). 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: * null * undefined -The options are the same as the options for `new Intl.DisplayNames()`. For a list of the options, see +The options are a subset of the options for `new Intl.DisplayNames()`. For a list of the options, see their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#options). 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: * null * undefined -The options are the same as the options for `new Intl.DisplayNames()`. For a list of the options, see +The options are a subset of the options for `new Intl.DisplayNames()`. For a list of the options, see their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DisplayNames/DisplayNames#options). With the `INTL_COUNTRY_PIPE_DEFAULT_OPTIONS` injection token you can specify default options. @@ -189,11 +189,30 @@ The unit parameter is required, see the [specification](https://tc39.es/proposal-unified-intl-numberformat/section6/locales-currencies-tz_proposed_out.html#sec-issanctionedsimpleunitidentifier) for a full list of possible values. 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 +The options are a subset of 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#options). With the `INTL_UNIT_PIPE_DEFAULT_OPTIONS` injection token you can specify default options. +### List pipe + +Use the list pipe like the following: + +``` +{{['my', 'items'] | intlList: options}} +``` + +The input can be one of the following: + +* Iterable of strings +* null +* undefined + +The options are a subset of the options for `new Intl.ListFormat()`. For a list of the options, see +their [docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/ListFormat/ListFormat#options). + +With the `INTL_LIST_PIPE_DEFAULT_OPTIONS` injection token you can specify default options. + ## Background For more context, see the following [GitHub issue](https://github.com/angular/angular/issues/49143) @@ -201,6 +220,5 @@ For more context, see the following [GitHub issue](https://github.com/angular/an ## Feature Roadmap * Performance: Prepare Intl.* object with default options, only construct new object when necessary -* List pipe * Relative time pipe * Migration Schematics for usages of Angular pipes diff --git a/projects/angular-ecmascript-intl/src/lib/intl.module.ts b/projects/angular-ecmascript-intl/src/lib/intl.module.ts index a2a8be68..d7b77e51 100644 --- a/projects/angular-ecmascript-intl/src/lib/intl.module.ts +++ b/projects/angular-ecmascript-intl/src/lib/intl.module.ts @@ -6,6 +6,7 @@ import {IntlPercentPipe} from "./percent/intl-percent.pipe"; import {IntlCurrencyPipe} from "./currency/intl-currency.pipe"; import {IntlCountryPipe} from "./country/intl-country.pipe"; import {IntlUnitPipe} from "./unit/intl-unit.pipe"; +import {IntlListPipe} from "./list/intl-list.pipe"; @NgModule({ imports: [ @@ -16,6 +17,7 @@ import {IntlUnitPipe} from "./unit/intl-unit.pipe"; IntlCurrencyPipe, IntlCountryPipe, IntlUnitPipe, + IntlListPipe, ], exports: [ IntlDatePipe, @@ -25,6 +27,7 @@ import {IntlUnitPipe} from "./unit/intl-unit.pipe"; IntlCurrencyPipe, IntlCountryPipe, IntlUnitPipe, + IntlListPipe, ], }) export class IntlModule { diff --git a/projects/angular-ecmascript-intl/src/lib/list/intl-list-pipe-default-options.ts b/projects/angular-ecmascript-intl/src/lib/list/intl-list-pipe-default-options.ts new file mode 100644 index 00000000..f6b7ad58 --- /dev/null +++ b/projects/angular-ecmascript-intl/src/lib/list/intl-list-pipe-default-options.ts @@ -0,0 +1,4 @@ +import {InjectionToken} from "@angular/core"; +import {IntlListPipeOptions} from "./intl-list.pipe"; + +export const INTL_LIST_PIPE_DEFAULT_OPTIONS = new InjectionToken>('IntlListPipeDefaultOptions'); diff --git a/projects/angular-ecmascript-intl/src/lib/list/intl-list.pipe.spec.ts b/projects/angular-ecmascript-intl/src/lib/list/intl-list.pipe.spec.ts new file mode 100644 index 00000000..3c904da8 --- /dev/null +++ b/projects/angular-ecmascript-intl/src/lib/list/intl-list.pipe.spec.ts @@ -0,0 +1,83 @@ +import {IntlListPipe} from './intl-list.pipe'; +import {TestBed} from "@angular/core/testing"; +import {INTL_LOCALES} from "../locale"; + +describe('IntlListPipe', () => { + let testUnit: IntlListPipe; + + describe('parsing', () => { + beforeEach(() => { + testUnit = new IntlListPipe('en-US'); + }); + + it('should create an instance', () => { + expect(testUnit).toBeTruthy(); + }); + + it('should handle null values', () => { + expect(testUnit.transform(null)).toBeNull(); + }); + + it('should handle undefined values', () => { + expect(testUnit.transform(undefined)).toBeNull(); + }); + + it('should handle empty arrays', () => { + expect(testUnit.transform([])).toEqual(''); + }); + + it('should transform string arrays', () => { + expect(testUnit.transform(['apples', 'pies'])).toEqual('apples and pies'); + }); + + it('should handle missing Intl.DisplayNames browser API', () => { + // @ts-expect-error Intl APIs are not expected to be undefined + spyOn(Intl, 'ListFormat').and.returnValue(undefined); + const consoleError = spyOn(console, 'error'); + + expect(testUnit.transform(['some', 'val'])).toBeNull(); + expect(consoleError).toHaveBeenCalledTimes(1); + }); + }); + + describe('internationalization', () => { + it('should respect the set locale', () => { + TestBed.configureTestingModule({ + providers: [ + IntlListPipe, + { + provide: INTL_LOCALES, + useValue: 'de-DE', + }, + ], + }); + testUnit = TestBed.inject(IntlListPipe); + + expect(testUnit.transform(['Äpfel', 'Birnen'])).toEqual('Äpfel und Birnen'); + }); + + it('should fall back to the browser default locale', () => { + TestBed.configureTestingModule({providers: [IntlListPipe]}); + + const result1 = TestBed.inject(IntlListPipe).transform(['some', 'val']); + const result2 = new IntlListPipe(navigator.language).transform(['some', 'val']); + + expect(result1).toEqual(result2); + }); + }); + + it('should respect locale option', () => { + TestBed.configureTestingModule({ + providers: [ + IntlListPipe, + { + provide: INTL_LOCALES, + useValue: 'en-US', + }, + ], + }); + testUnit = TestBed.inject(IntlListPipe); + + expect(testUnit.transform(['Äpfel', 'Birnen'], {locale: 'de-DE'})).toEqual('Äpfel und Birnen'); + }); +}); diff --git a/projects/angular-ecmascript-intl/src/lib/list/intl-list.pipe.ts b/projects/angular-ecmascript-intl/src/lib/list/intl-list.pipe.ts new file mode 100644 index 00000000..83ed957a --- /dev/null +++ b/projects/angular-ecmascript-intl/src/lib/list/intl-list.pipe.ts @@ -0,0 +1,35 @@ +import {Inject, Optional, Pipe, PipeTransform} from '@angular/core'; +import {INTL_LOCALES} from "../locale"; +import {INTL_LIST_PIPE_DEFAULT_OPTIONS} from "./intl-list-pipe-default-options"; +import {IntlPipeOptions} from "../intl-pipe-options"; + +export type IntlListPipeOptions = Partial & IntlPipeOptions; + +@Pipe({ + name: 'intlList', + standalone: true, +}) +export class IntlListPipe implements PipeTransform { + + constructor(@Optional() @Inject(INTL_LOCALES) readonly locale?: string | string[] | null, + @Optional() @Inject(INTL_LIST_PIPE_DEFAULT_OPTIONS) readonly defaultOptions?: Omit | null) { + } + + transform(value: Iterable | null | undefined, options?: IntlListPipeOptions): string | null { + if (!value) { + return null; + } + + const {locale, ...intlOptions} = options ?? {}; + + try { + return new Intl.ListFormat(locale ?? this.locale ?? undefined, { + ...this.defaultOptions, ...intlOptions, + }).format(value); + } catch (e) { + console.error('Error while transforming the list', e); + return null; + } + } + +} diff --git a/projects/angular-ecmascript-intl/src/lib/unit/intl-unit.pipe.ts b/projects/angular-ecmascript-intl/src/lib/unit/intl-unit.pipe.ts index b98662e9..bf93dab1 100644 --- a/projects/angular-ecmascript-intl/src/lib/unit/intl-unit.pipe.ts +++ b/projects/angular-ecmascript-intl/src/lib/unit/intl-unit.pipe.ts @@ -32,7 +32,7 @@ export class IntlUnitPipe implements PipeTransform { {...this.defaultOptions, ...intlOptions, unit, style: 'unit'}, ).format(numericValue); } catch (e) { - console.error('Error while transforming the percent value', e); + console.error('Error while transforming the unit value', e); return null; } } diff --git a/projects/angular-ecmascript-intl/src/public-api.ts b/projects/angular-ecmascript-intl/src/public-api.ts index 0e0e644b..55524075 100644 --- a/projects/angular-ecmascript-intl/src/public-api.ts +++ b/projects/angular-ecmascript-intl/src/public-api.ts @@ -13,6 +13,8 @@ export * from './lib/decimal/intl-decimal-pipe-default-options'; export * from './lib/intl.module'; export * from './lib/language/intl-language.pipe'; export * from './lib/language/intl-language-pipe-default-options'; +export * from './lib/list/intl-list.pipe'; +export * from './lib/list/intl-list-pipe-default-options'; export * from './lib/locale'; export * from './lib/percent/intl-percent.pipe'; export * from './lib/percent/intl-percent-pipe-default-options'; diff --git a/projects/angular-intl-demo/src/app/pipes/list/list.component.html b/projects/angular-intl-demo/src/app/pipes/list/list.component.html new file mode 100644 index 00000000..7e7532e8 --- /dev/null +++ b/projects/angular-intl-demo/src/app/pipes/list/list.component.html @@ -0,0 +1,38 @@ +
+ + List items + + {{item}} + + + + + Locale + + Browser default + {{language}} + + + + + Type + + Browser default + conjunction + disjunction + unit + + + + + Style + + Browser default + long + short + narrow + + +
+ +

{{selectedItems | intlList: {locale, type, style} }}

diff --git a/projects/angular-intl-demo/src/app/pipes/list/list.component.scss b/projects/angular-intl-demo/src/app/pipes/list/list.component.scss new file mode 100644 index 00000000..d1ecbe60 --- /dev/null +++ b/projects/angular-intl-demo/src/app/pipes/list/list.component.scss @@ -0,0 +1,7 @@ +.fields-container { + display: flex; + gap: 16px; + flex-wrap: wrap; + align-items: center; + margin-bottom: 16px; +} diff --git a/projects/angular-intl-demo/src/app/pipes/list/list.component.ts b/projects/angular-intl-demo/src/app/pipes/list/list.component.ts new file mode 100644 index 00000000..ea7dfe17 --- /dev/null +++ b/projects/angular-intl-demo/src/app/pipes/list/list.component.ts @@ -0,0 +1,18 @@ +import {Component} from '@angular/core'; +import {languages} from "../../languages"; +import {list} from "./list"; +import {IntlListPipeOptions} from "projects/angular-ecmascript-intl/src/lib/list/intl-list.pipe"; + +@Component({ + selector: 'app-list', + templateUrl: './list.component.html', + styleUrls: ['./list.component.scss'], +}) +export class ListComponent { + languages = languages; + list = list; + selectedItems: string[] = [list[0], list[2], list[3]]; + locale?: string; + type?: IntlListPipeOptions['type']; + style?: IntlListPipeOptions['style']; +} diff --git a/projects/angular-intl-demo/src/app/pipes/list/list.ts b/projects/angular-intl-demo/src/app/pipes/list/list.ts new file mode 100644 index 00000000..013c5971 --- /dev/null +++ b/projects/angular-intl-demo/src/app/pipes/list/list.ts @@ -0,0 +1,7 @@ +export const list = [ + 'Pizza', + 'Lasagne', + 'Gnocchi', + 'Spaghetti', + 'Pesto', +]; diff --git a/projects/angular-intl-demo/src/app/pipes/pipes-routing.module.ts b/projects/angular-intl-demo/src/app/pipes/pipes-routing.module.ts index dc31dfeb..c1281904 100644 --- a/projects/angular-intl-demo/src/app/pipes/pipes-routing.module.ts +++ b/projects/angular-intl-demo/src/app/pipes/pipes-routing.module.ts @@ -8,6 +8,7 @@ import {CurrencyComponent} from "./currency/currency.component"; import {LanguageComponent} from "./language/language.component"; import {CountryComponent} from "./country/country.component"; import {UnitComponent} from "./unit/unit.component"; +import {ListComponent} from "./list/list.component"; const routes: Routes = [ { @@ -42,6 +43,10 @@ const routes: Routes = [ path: 'country', component: CountryComponent, }, + { + path: 'list', + component: ListComponent, + }, { path: '', redirectTo: 'date', diff --git a/projects/angular-intl-demo/src/app/pipes/pipes.component.html b/projects/angular-intl-demo/src/app/pipes/pipes.component.html index 0ac5dcd3..86985805 100644 --- a/projects/angular-intl-demo/src/app/pipes/pipes.component.html +++ b/projects/angular-intl-demo/src/app/pipes/pipes.component.html @@ -13,6 +13,8 @@ routerLinkActive>Language Country + List diff --git a/projects/angular-intl-demo/src/app/pipes/pipes.module.ts b/projects/angular-intl-demo/src/app/pipes/pipes.module.ts index f07816a8..4d47cf45 100644 --- a/projects/angular-intl-demo/src/app/pipes/pipes.module.ts +++ b/projects/angular-intl-demo/src/app/pipes/pipes.module.ts @@ -16,6 +16,7 @@ import {CountryComponent} from "./country/country.component"; import {NgxMatDatetimePickerModule, NgxMatNativeDateModule} from "@angular-material-components/datetime-picker"; import {MatDatepickerModule} from "@angular/material/datepicker"; import {UnitComponent} from "./unit/unit.component"; +import {ListComponent} from "./list/list.component"; @NgModule({ declarations: [ @@ -27,6 +28,7 @@ import {UnitComponent} from "./unit/unit.component"; PipesComponent, CountryComponent, UnitComponent, + ListComponent, ], imports: [ CommonModule,