Browse Source

Feature/add country flag to currency selector (#7561)

* Add country flag

* Update changelog
pull/7553/head^2
Thomas Kaul 1 day ago
committed by GitHub
parent
commit
901b52393f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      CHANGELOG.md
  2. 25
      libs/common/src/lib/helper.spec.ts
  3. 11
      libs/common/src/lib/helper.ts
  4. 10
      libs/ui/src/lib/currency-selector/currency-selector.component.html
  5. 3
      libs/ui/src/lib/currency-selector/currency-selector.component.scss
  6. 99
      libs/ui/src/lib/currency-selector/currency-selector.component.stories.ts
  7. 30
      libs/ui/src/lib/currency-selector/currency-selector.component.ts

5
CHANGELOG.md

@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added the country flag to the currency selector
- Added a _Storybook_ story for the currency selector component
### Changed ### Changed
- Migrated the abstract _Material_ form field from a component to a directive - Migrated the abstract _Material_ form field from a component to a directive

25
libs/common/src/lib/helper.spec.ts

@ -4,6 +4,7 @@ import {
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { import {
extractNumberFromString, extractNumberFromString,
getCountryCodeFromCurrency,
getNumberFormatGroup, getNumberFormatGroup,
getStringOrNull, getStringOrNull,
getStringOrUndefined, 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', () => { describe('Get number format group', () => {
let languageGetter: jest.SpyInstance<string, [], any>; let languageGetter: jest.SpyInstance<string, [], any>;

11
libs/common/src/lib/helper.ts

@ -279,6 +279,17 @@ export function getCurrencyFromSymbol(aSymbol = '') {
return aSymbol.replace(DEFAULT_CURRENCY, ''); 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 { export function getCountryName({ code }: { code: string }): string {
try { try {
return ( return (

10
libs/ui/src/lib/currency-selector/currency-selector.component.html

@ -1,9 +1,14 @@
@if (emojiFlagOfSelectedCurrency) {
<span class="flex-shrink-0 mr-1">{{ emojiFlagOfSelectedCurrency }}</span>
}
<input <input
autocapitalize="off" autocapitalize="off"
autocomplete="off" autocomplete="off"
matInput matInput
[formControl]="control" [formControl]="control"
[matAutocomplete]="currencyAutocomplete" [matAutocomplete]="currencyAutocomplete"
[matAutocompleteConnectedTo]="autocompleteOrigin"
/> />
<mat-autocomplete <mat-autocomplete
@ -12,7 +17,10 @@
> >
@for (currency of filteredCurrencies; track currency) { @for (currency of filteredCurrencies; track currency) {
<mat-option class="line-height-1" [value]="currency"> <mat-option class="line-height-1" [value]="currency">
{{ currency }} <span class="align-items-center d-flex">
<span class="mr-1">{{ getEmojiFlagFromCurrency(currency) }}</span>
<span>{{ currency }}</span>
</span>
</mat-option> </mat-option>
} }
</mat-autocomplete> </mat-autocomplete>

3
libs/ui/src/lib/currency-selector/currency-selector.component.scss

@ -1,3 +0,0 @@
:host {
display: block;
}

99
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<GfCurrencySelectorComponent> = {
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: `
<form [formGroup]="formGroup">
<mat-form-field appearance="outline" class="w-100">
<mat-label>Currency</mat-label>
<gf-currency-selector
formControlName="currency"
[currencies]="currencies"
/>
</mat-form-field>
</form>
`
};
}
};
export default meta;
type Story = StoryObj<GfCurrencySelectorComponent>;
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
}
};

30
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 { FocusMonitor } from '@angular/cdk/a11y';
import { import {
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
@ -24,9 +29,11 @@ import {
import { import {
MatAutocomplete, MatAutocomplete,
MatAutocompleteModule, MatAutocompleteModule,
MatAutocompleteOrigin,
MatOption MatOption
} from '@angular/material/autocomplete'; } from '@angular/material/autocomplete';
import { import {
MAT_FORM_FIELD,
MatFormFieldControl, MatFormFieldControl,
MatFormFieldModule MatFormFieldModule
} from '@angular/material/form-field'; } from '@angular/material/form-field';
@ -39,7 +46,8 @@ import { AbstractMatFormField } from '../shared/abstract-mat-form-field';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
host: { host: {
'[attr.aria-describedBy]': 'describedBy', '[attr.aria-describedBy]': 'describedBy',
'[id]': 'id' '[id]': 'id',
class: 'align-items-center d-flex'
}, },
imports: [ imports: [
FormsModule, FormsModule,
@ -56,7 +64,6 @@ import { AbstractMatFormField } from '../shared/abstract-mat-form-field';
], ],
schemas: [CUSTOM_ELEMENTS_SCHEMA], schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-currency-selector', selector: 'gf-currency-selector',
styleUrls: ['./currency-selector.component.scss'],
templateUrl: 'currency-selector.component.html' templateUrl: 'currency-selector.component.html'
}) })
export class GfCurrencySelectorComponent export class GfCurrencySelectorComponent
@ -72,6 +79,7 @@ export class GfCurrencySelectorComponent
public readonly formControlName = input.required<string>(); public readonly formControlName = input.required<string>();
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly formField = inject(MAT_FORM_FIELD);
private readonly input = viewChild.required(MatInput); private readonly input = viewChild.required(MatInput);
public constructor( public constructor(
@ -86,8 +94,20 @@ export class GfCurrencySelectorComponent
this.controlType = 'currency-selector'; 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() { public override get empty() {
return this.input().empty; return !this.control.value;
} }
public override set value(value: string | null) { public override set value(value: string | null) {
@ -99,6 +119,10 @@ export class GfCurrencySelectorComponent
this.input().focus(); this.input().focus();
} }
public getEmojiFlagFromCurrency(aCurrency = '') {
return getEmojiFlag(getCountryCodeFromCurrency(aCurrency));
}
public ngOnInit() { public ngOnInit() {
if (this.disabled) { if (this.disabled) {
this.control.disable(); this.control.disable();

Loading…
Cancel
Save