diff --git a/CHANGELOG.md b/CHANGELOG.md index 736c708e2..32b949274 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added the country flag to the currency selector +- Added a _Storybook_ story for the currency selector component + ### Changed - Migrated the abstract _Material_ form field from a component to a directive diff --git a/libs/common/src/lib/helper.spec.ts b/libs/common/src/lib/helper.spec.ts index 669c42e32..6cc090170 100644 --- a/libs/common/src/lib/helper.spec.ts +++ b/libs/common/src/lib/helper.spec.ts @@ -4,6 +4,7 @@ import { } from '@ghostfolio/common/config'; import { extractNumberFromString, + getCountryCodeFromCurrency, getNumberFormatGroup, getStringOrNull, getStringOrUndefined, @@ -77,6 +78,30 @@ describe('Helper', () => { }); }); + describe('Get country code from currency', () => { + it('ISO 4217 currency code', () => { + expect(getCountryCodeFromCurrency('CHF')).toEqual('CH'); + expect(getCountryCodeFromCurrency('USD')).toEqual('US'); + }); + + it('Currency of the European Union', () => { + expect(getCountryCodeFromCurrency('EUR')).toEqual('EU'); + }); + + it('Derived currency', () => { + expect(getCountryCodeFromCurrency('GBp')).toEqual('GB'); + }); + + it('Supranational currency', () => { + expect(getCountryCodeFromCurrency('XAU')).toEqual(''); + expect(getCountryCodeFromCurrency('XOF')).toEqual(''); + }); + + it('Empty currency', () => { + expect(getCountryCodeFromCurrency('')).toEqual(''); + }); + }); + describe('Get number format group', () => { let languageGetter: jest.SpyInstance; diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 0778f84f1..44fd89aa2 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -279,6 +279,17 @@ export function getCurrencyFromSymbol(aSymbol = '') { return aSymbol.replace(DEFAULT_CURRENCY, ''); } +export function getCountryCodeFromCurrency(aCurrency = '') { + // An ISO 4217 currency code is composed of the ISO 3166-1 alpha-2 country + // code and the initial of the currency itself, except for the supranational + // currencies, which are prefixed with X (like XAU or XOF) + if (aCurrency.startsWith('X')) { + return ''; + } + + return aCurrency.slice(0, 2).toUpperCase(); +} + export function getCountryName({ code }: { code: string }): string { try { return ( diff --git a/libs/ui/src/lib/currency-selector/currency-selector.component.html b/libs/ui/src/lib/currency-selector/currency-selector.component.html index e07101f9a..594515e24 100644 --- a/libs/ui/src/lib/currency-selector/currency-selector.component.html +++ b/libs/ui/src/lib/currency-selector/currency-selector.component.html @@ -1,9 +1,14 @@ +@if (emojiFlagOfSelectedCurrency) { + {{ emojiFlagOfSelectedCurrency }} +} + @for (currency of filteredCurrencies; track currency) { - {{ currency }} + + {{ getEmojiFlagFromCurrency(currency) }} + {{ currency }} + } diff --git a/libs/ui/src/lib/currency-selector/currency-selector.component.scss b/libs/ui/src/lib/currency-selector/currency-selector.component.scss deleted file mode 100644 index 5d4e87f30..000000000 --- a/libs/ui/src/lib/currency-selector/currency-selector.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -:host { - display: block; -} diff --git a/libs/ui/src/lib/currency-selector/currency-selector.component.stories.ts b/libs/ui/src/lib/currency-selector/currency-selector.component.stories.ts new file mode 100644 index 000000000..beb63e369 --- /dev/null +++ b/libs/ui/src/lib/currency-selector/currency-selector.component.stories.ts @@ -0,0 +1,99 @@ +import { ANIMATION_MODULE_TYPE } from '@angular/core'; +import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms'; +import '@angular/localize/init'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'; + +import { GfCurrencySelectorComponent } from './currency-selector.component'; + +const CURRENCIES = [ + 'AUD', + 'CHF', + 'EUR', + 'GBP', + 'GBp', + 'JPY', + 'USD', + 'XAU', + 'ZAR' +]; + +const meta: Meta = { + title: 'Currency Selector', + component: GfCurrencySelectorComponent, + decorators: [ + moduleMetadata({ + imports: [ + GfCurrencySelectorComponent, + MatFormFieldModule, + ReactiveFormsModule + ], + providers: [ + { + provide: ANIMATION_MODULE_TYPE, + useValue: 'NoopAnimations' + } + ] + }) + ], + render: ({ currencies, value }) => { + return { + props: { + currencies, + formGroup: new FormGroup({ + currency: new FormControl(value) + }) + }, + template: ` +
+ + Currency + + +
+ ` + }; + } +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + currencies: CURRENCIES, + value: 'CHF' + } +}; + +export const CurrencyOfEuropeanUnion: Story = { + args: { + currencies: CURRENCIES, + value: 'EUR' + } +}; + +export const DerivedCurrency: Story = { + args: { + currencies: CURRENCIES, + value: 'GBp' + } +}; + +export const SupranationalCurrency: Story = { + args: { + currencies: CURRENCIES, + value: 'XAU' + } +}; + +export const WithoutValue: Story = { + args: { + currencies: CURRENCIES, + value: null + } +}; diff --git a/libs/ui/src/lib/currency-selector/currency-selector.component.ts b/libs/ui/src/lib/currency-selector/currency-selector.component.ts index 724e86712..2eda80aba 100644 --- a/libs/ui/src/lib/currency-selector/currency-selector.component.ts +++ b/libs/ui/src/lib/currency-selector/currency-selector.component.ts @@ -1,3 +1,8 @@ +import { + getCountryCodeFromCurrency, + getEmojiFlag +} from '@ghostfolio/common/helper'; + import { FocusMonitor } from '@angular/cdk/a11y'; import { CUSTOM_ELEMENTS_SCHEMA, @@ -24,9 +29,11 @@ import { import { MatAutocomplete, MatAutocompleteModule, + MatAutocompleteOrigin, MatOption } from '@angular/material/autocomplete'; import { + MAT_FORM_FIELD, MatFormFieldControl, MatFormFieldModule } from '@angular/material/form-field'; @@ -39,7 +46,8 @@ import { AbstractMatFormField } from '../shared/abstract-mat-form-field'; changeDetection: ChangeDetectionStrategy.OnPush, host: { '[attr.aria-describedBy]': 'describedBy', - '[id]': 'id' + '[id]': 'id', + class: 'align-items-center d-flex' }, imports: [ FormsModule, @@ -56,7 +64,6 @@ import { AbstractMatFormField } from '../shared/abstract-mat-form-field'; ], schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-currency-selector', - styleUrls: ['./currency-selector.component.scss'], templateUrl: 'currency-selector.component.html' }) export class GfCurrencySelectorComponent @@ -72,6 +79,7 @@ export class GfCurrencySelectorComponent public readonly formControlName = input.required(); private readonly destroyRef = inject(DestroyRef); + private readonly formField = inject(MAT_FORM_FIELD); private readonly input = viewChild.required(MatInput); public constructor( @@ -86,8 +94,20 @@ export class GfCurrencySelectorComponent this.controlType = 'currency-selector'; } + public get autocompleteOrigin(): MatAutocompleteOrigin { + return { elementRef: this.formField.getConnectedOverlayOrigin() }; + } + + public get emojiFlagOfSelectedCurrency() { + const selectedCurrency = this.currencies().find((currency) => { + return currency === this.control.value; + }); + + return this.getEmojiFlagFromCurrency(selectedCurrency); + } + public override get empty() { - return this.input().empty; + return !this.control.value; } public override set value(value: string | null) { @@ -99,6 +119,10 @@ export class GfCurrencySelectorComponent this.input().focus(); } + public getEmojiFlagFromCurrency(aCurrency = '') { + return getEmojiFlag(getCountryCodeFromCurrency(aCurrency)); + } + public ngOnInit() { if (this.disabled) { this.control.disable();