diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bd3340ec..ba514ecef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -241,7 +241,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Restricted the symbol data endpoint (`GET /api/v1/symbol/:dataSource/:symbol`) to authenticated users - Removed the deprecated `auth` endpoint of the login with _Security Token_ (`GET`) - Simplified the `getHistorical()` function response in the data provider interface +<<<<<<< HEAD - Upgraded `bull-board` from version `8.0.1` to `8.1.2` +======= +- Fixed parsing of localized decimal numbers with thousands separators across number input fields +>>>>>>> c5ab9c84a (fix(client): address review feedback for localized number directive) ## 3.29.0 - 2026-07-18 diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts index aa61845f4..d4dc44bb4 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts @@ -32,6 +32,7 @@ import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo'; import { GfHistoricalMarketDataEditorComponent } from '@ghostfolio/ui/historical-market-data-editor'; import { translate } from '@ghostfolio/ui/i18n'; import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; +import { GfLocalizedNumberDirective } from '@ghostfolio/ui/localized-number'; import { NotificationService } from '@ghostfolio/ui/notifications'; import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart'; import { AdminService, DataService } from '@ghostfolio/ui/services'; @@ -119,6 +120,7 @@ import { AssetProfileDialogParams } from './interfaces/interfaces'; GfEntityLogoComponent, GfHistoricalMarketDataEditorComponent, GfLineChartComponent, + GfLocalizedNumberDirective, GfPortfolioProportionChartComponent, GfSymbolAutocompleteComponent, GfValueComponent, diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html index f80cec842..7a89afe69 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -638,8 +638,9 @@ Default Market Price diff --git a/apps/client/src/app/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts index 291a7f3f9..bd89ed072 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -328,7 +328,8 @@ export class GfAccountsPageComponent implements OnInit { TransferBalanceDialogParams >(GfTransferBalanceDialogComponent, { data: { - accounts: this.accounts + accounts: this.accounts, + locale: this.user?.settings?.locale }, width: this.deviceType() === 'mobile' ? '100vw' : '50rem' }); diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts index 2157ea5d3..2f0c699b9 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts @@ -1,5 +1,8 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { TAG_ID_EXCLUDE_FROM_ANALYSIS } from '@ghostfolio/common/config'; +import { + DEFAULT_LOCALE, + TAG_ID_EXCLUDE_FROM_ANALYSIS +} from '@ghostfolio/common/config'; import { CreateAccountDto, UpdateAccountDto } from '@ghostfolio/common/dtos'; import { getStringOrNull } from '@ghostfolio/common/helper'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; @@ -7,6 +10,7 @@ import { validateObjectForForm } from '@ghostfolio/common/utils'; import { GfCurrencySelectorComponent } from '@ghostfolio/ui/currency-selector'; import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo'; import { translate } from '@ghostfolio/ui/i18n'; +import { GfLocalizedNumberDirective } from '@ghostfolio/ui/localized-number'; import { DataService } from '@ghostfolio/ui/services'; import { GfTagsSelectorComponent } from '@ghostfolio/ui/tags-selector'; @@ -48,6 +52,7 @@ import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces'; CommonModule, GfCurrencySelectorComponent, GfEntityLogoComponent, + GfLocalizedNumberDirective, GfTagsSelectorComponent, MatAutocompleteModule, MatButtonModule, @@ -76,15 +81,18 @@ export class GfCreateOrUpdateAccountDialogComponent { inject>(MatDialogRef); private readonly formBuilder = inject(FormBuilder); private readonly userService = inject(UserService); + protected locale = inject(MAT_DATE_LOCALE); public ngOnInit() { const { currencies } = this.dataService.fetchInfo(); this.currencies = currencies; - this.hasPermissionToCreateOwnTag = hasPermission( - this.data.user?.permissions, - permissions.createOwnTag - ); + this.locale = + this.data.user?.settings?.locale ?? this.locale ?? DEFAULT_LOCALE; + + this.hasPermissionToCreateOwnTag = + this.data.user?.settings?.isExperimentalFeatures && + hasPermission(this.data.user?.permissions, permissions.createOwnTag); this.tagsAvailable = [ ...(this.data.user?.tags ?? []), diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html index 4e4c62b6f..4af0eac1a 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -34,8 +34,9 @@ Cash Balance {{ diff --git a/apps/client/src/app/pages/accounts/transfer-balance/interfaces/interfaces.ts b/apps/client/src/app/pages/accounts/transfer-balance/interfaces/interfaces.ts index 51c42bc5d..84d61e580 100644 --- a/apps/client/src/app/pages/accounts/transfer-balance/interfaces/interfaces.ts +++ b/apps/client/src/app/pages/accounts/transfer-balance/interfaces/interfaces.ts @@ -3,6 +3,7 @@ import { Account } from '@prisma/client'; export interface TransferBalanceDialogParams { accounts: Account[]; + locale?: string; } export type TransferBalanceForm = FormGroup<{ diff --git a/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts b/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts index cbf0e460d..b36879563 100644 --- a/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts +++ b/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.component.ts @@ -1,5 +1,6 @@ import { TransferBalanceDto } from '@ghostfolio/common/dtos'; import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo'; +import { GfLocalizedNumberDirective } from '@ghostfolio/ui/localized-number'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; import { @@ -10,6 +11,7 @@ import { Validators } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; +import { MAT_DATE_LOCALE } from '@angular/material/core'; import { MAT_DIALOG_DATA, MatDialogModule, @@ -18,7 +20,6 @@ import { import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatSelectModule } from '@angular/material/select'; -import { Account } from '@prisma/client'; import { TransferBalanceDialogParams, @@ -30,6 +31,7 @@ import { host: { class: 'h-100' }, imports: [ GfEntityLogoComponent, + GfLocalizedNumberDirective, MatButtonModule, MatDialogModule, MatFormFieldModule, @@ -42,10 +44,12 @@ import { templateUrl: 'transfer-balance-dialog.html' }) export class GfTransferBalanceDialogComponent { - protected readonly accounts: Account[] = - inject(MAT_DIALOG_DATA).accounts; + private readonly data = inject(MAT_DIALOG_DATA); + + protected readonly accounts = this.data.accounts; protected currency: string; + protected locale = this.data.locale ?? inject(MAT_DATE_LOCALE); protected readonly transferBalanceForm: TransferBalanceForm = new FormGroup( { diff --git a/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.html b/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.html index 50c96be86..24f85a8a0 100644 --- a/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.html +++ b/apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.html @@ -53,8 +53,9 @@ Value {{ currency }} diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index 3b80d2825..eb870acb6 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -13,6 +13,7 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { validateObjectForForm } from '@ghostfolio/common/utils'; import { GfEntityLogoComponent } from '@ghostfolio/ui/entity-logo'; import { translate } from '@ghostfolio/ui/i18n'; +import { GfLocalizedNumberDirective } from '@ghostfolio/ui/localized-number'; import { DataService } from '@ghostfolio/ui/services'; import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete'; import { GfTagsSelectorComponent } from '@ghostfolio/ui/tags-selector'; @@ -60,6 +61,7 @@ import { ActivityType } from './types/activity-type.type'; host: { class: 'h-100' }, imports: [ GfEntityLogoComponent, + GfLocalizedNumberDirective, GfSymbolAutocompleteComponent, GfTagsSelectorComponent, GfValueComponent, @@ -114,7 +116,7 @@ export class GfCreateOrUpdateActivityDialogComponent { private readonly dialogRef = inject>(MatDialogRef); private readonly formBuilder = inject(FormBuilder); - private locale = inject(MAT_DATE_LOCALE); + protected locale = inject(MAT_DATE_LOCALE); private readonly userService = inject(UserService); public constructor() { @@ -127,7 +129,8 @@ export class GfCreateOrUpdateActivityDialogComponent { this.data.user?.permissions, permissions.createOwnTag ); - this.locale = this.data.user.settings.locale ?? DEFAULT_LOCALE; + this.locale = + this.data.user.settings.locale ?? this.locale ?? DEFAULT_LOCALE; this.mode = this.data.activity?.id ? 'update' : 'create'; this.dateAdapter.setLocale(this.locale); diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html index b5dbf4669..c41642968 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -181,7 +181,12 @@ > Quantity - +
- +
Fee - +
{ ).toEqual(-99999.99); }); + it('Get decimal number with thousands separator (de-DE)', () => { + expect( + extractNumberFromString({ locale: 'de-DE', value: '1.234,50' }) + ).toEqual(1234.5); + expect( + extractNumberFromString({ locale: 'de-DE', value: '1234,50' }) + ).toEqual(1234.5); + expect( + extractNumberFromString({ locale: 'de-DE', value: '12.345.678,90' }) + ).toEqual(12345678.9); + }); + + it('Get decimal number with thousands separator (en-US)', () => { + expect( + extractNumberFromString({ locale: 'en-US', value: '1,234.50' }) + ).toEqual(1234.5); + expect( + extractNumberFromString({ locale: 'en-US', value: '1234.50' }) + ).toEqual(1234.5); + }); + it('Get decimal number (comma notation) for locale where currency is not grouped by default', () => { expect( extractNumberFromString({ locale: 'es-ES', value: '999,99' }) diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 22f969d02..98331a88a 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -1,4 +1,4 @@ -import { NumberParser } from '@internationalized/number'; +import { NumberFormatter, NumberParser } from '@internationalized/number'; import { Type as ActivityType, AssetProfileOverrides, @@ -234,6 +234,7 @@ export function extractNumberFromString({ } } +<<<<<<< HEAD export function formatMonthAndYear({ date, locale @@ -245,6 +246,20 @@ export function formatMonthAndYear({ month: 'long', year: 'numeric' }).format(date); +======= +export function formatNumberForLocale({ + locale = 'en-US', + value +}: { + locale?: string; + value: number; +}): string { + const formatter = new NumberFormatter(locale, { + maximumFractionDigits: 15 + }); + + return formatter.format(value); +>>>>>>> c5ab9c84a (fix(client): address review feedback for localized number directive) } export function getAllActivityTypes(): ActivityType[] { diff --git a/libs/ui/src/lib/account-balances/account-balances.component.html b/libs/ui/src/lib/account-balances/account-balances.component.html index 780653c67..d972eea69 100644 --- a/libs/ui/src/lib/account-balances/account-balances.component.html +++ b/libs/ui/src/lib/account-balances/account-balances.component.html @@ -51,7 +51,12 @@
- +
{{ accountCurrency() }}
diff --git a/libs/ui/src/lib/account-balances/account-balances.component.ts b/libs/ui/src/lib/account-balances/account-balances.component.ts index e27d29516..2742d9fd4 100644 --- a/libs/ui/src/lib/account-balances/account-balances.component.ts +++ b/libs/ui/src/lib/account-balances/account-balances.component.ts @@ -42,11 +42,13 @@ import { } from 'ionicons/icons'; import { get, isNil } from 'lodash'; +import { GfLocalizedNumberDirective } from '../localized-number'; import { GfValueComponent } from '../value'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, imports: [ + GfLocalizedNumberDirective, GfValueComponent, IonIcon, MatButtonModule, diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts index 1b7b99fa2..525f4a44a 100644 --- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts +++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.component.ts @@ -27,6 +27,7 @@ import { addIcons } from 'ionicons'; import { calendarClearOutline, refreshOutline } from 'ionicons/icons'; import { isNil } from 'lodash'; +import { GfLocalizedNumberDirective } from '../../localized-number'; import { HistoricalMarketDataEditorDialogParams } from './interfaces/interfaces'; @Component({ @@ -34,6 +35,7 @@ import { HistoricalMarketDataEditorDialogParams } from './interfaces/interfaces' host: { class: 'h-100' }, imports: [ FormsModule, + GfLocalizedNumberDirective, IonIcon, MatButtonModule, MatDatepickerModule, @@ -54,7 +56,7 @@ export class GfHistoricalMarketDataEditorDialogComponent implements OnInit { protected readonly marketPrice = signal(this.data.marketPrice); private readonly destroyRef = inject(DestroyRef); - private readonly locale = + protected readonly locale = this.data.user.settings.locale ?? inject(MAT_DATE_LOCALE); public constructor( diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html index 7e8183664..858304eda 100644 --- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html +++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html @@ -25,9 +25,10 @@ Market Price diff --git a/libs/ui/src/lib/localized-number/index.ts b/libs/ui/src/lib/localized-number/index.ts new file mode 100644 index 000000000..ca2e9de13 --- /dev/null +++ b/libs/ui/src/lib/localized-number/index.ts @@ -0,0 +1 @@ +export * from './localized-number.directive'; diff --git a/libs/ui/src/lib/localized-number/localized-number.directive.spec.ts b/libs/ui/src/lib/localized-number/localized-number.directive.spec.ts new file mode 100644 index 000000000..087520a0d --- /dev/null +++ b/libs/ui/src/lib/localized-number/localized-number.directive.spec.ts @@ -0,0 +1,147 @@ +import { Component } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { FormControl, ReactiveFormsModule } from '@angular/forms'; + +import { GfLocalizedNumberDirective } from './localized-number.directive'; + +@Component({ + imports: [GfLocalizedNumberDirective, ReactiveFormsModule], + template: ` + + ` +}) +class TestHostComponent { + public control = new FormControl(null); + public locale = 'en-US'; +} + +describe('GfLocalizedNumberDirective', () => { + let fixture: ComponentFixture; + let host: TestHostComponent; + let input: HTMLInputElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [TestHostComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(TestHostComponent); + host = fixture.componentInstance; + fixture.detectChanges(); + input = fixture.nativeElement.querySelector('input'); + }); + + function typeValue(value: string) { + input.value = value; + input.dispatchEvent(new Event('input')); + fixture.detectChanges(); + } + + it('should force type="text" and inputmode="decimal"', () => { + expect(input.getAttribute('type')).toBe('text'); + expect(input.getAttribute('inputmode')).toBe('decimal'); + }); + + it('should parse English grouped numbers', () => { + host.locale = 'en-US'; + fixture.detectChanges(); + + typeValue('1,234.50'); + expect(host.control.value).toBe(1234.5); + + typeValue('1234.50'); + expect(host.control.value).toBe(1234.5); + }); + + it('should parse German grouped numbers', () => { + host.locale = 'de-DE'; + fixture.detectChanges(); + + typeValue('1.234,50'); + expect(host.control.value).toBe(1234.5); + + typeValue('1234,50'); + expect(host.control.value).toBe(1234.5); + + typeValue('12.345.678,90'); + expect(host.control.value).toBe(12345678.9); + }); + + it('should set null for empty or invalid input', () => { + typeValue(''); + expect(host.control.value).toBeNull(); + + typeValue(' '); + expect(host.control.value).toBeNull(); + + typeValue('abc'); + expect(host.control.value).toBeNull(); + }); + + it('should write programmatic values to the input', () => { + host.control.setValue(1234.5); + fixture.detectChanges(); + + expect(input.value).toBe('1,234.5'); + + host.control.setValue(null); + fixture.detectChanges(); + + expect(input.value).toBe(''); + }); + + it('should write and parse correctly for German locale (de-DE)', () => { + host.locale = 'de-DE'; + fixture.detectChanges(); + + host.control.setValue(1234.5); + fixture.detectChanges(); + + expect(input.value).toBe('1.234,5'); + + typeValue('1.234,5'); + + expect(host.control.value).toBe(1234.5); + }); + + it('should preserve more than 3 fraction digits on write/parse', () => { + host.locale = 'de-DE'; + fixture.detectChanges(); + + host.control.setValue(12.345678); + fixture.detectChanges(); + + expect(input.value).toBe('12,345678'); + + typeValue(input.value); + + expect(host.control.value).toBe(12.345678); + }); + + it('should write empty string for non-numeric values', () => { + host.control.setValue('' as unknown as number); + fixture.detectChanges(); + + expect(input.value).toBe(''); + }); + + it('should keep required validation working', () => { + host.locale = 'de-DE'; + host.control.setValidators([ + (control) => { + return control.value === null || control.value === undefined + ? { required: true } + : null; + } + ]); + host.control.updateValueAndValidity(); + fixture.detectChanges(); + + typeValue(''); + expect(host.control.hasError('required')).toBe(true); + + typeValue('1.234,50'); + expect(host.control.hasError('required')).toBe(false); + expect(host.control.value).toBe(1234.5); + }); +}); diff --git a/libs/ui/src/lib/localized-number/localized-number.directive.ts b/libs/ui/src/lib/localized-number/localized-number.directive.ts new file mode 100644 index 000000000..71928c754 --- /dev/null +++ b/libs/ui/src/lib/localized-number/localized-number.directive.ts @@ -0,0 +1,110 @@ +import { DEFAULT_LOCALE } from '@ghostfolio/common/config'; +import { + extractNumberFromString, + formatNumberForLocale +} from '@ghostfolio/common/helper'; + +import { DOCUMENT } from '@angular/common'; +import { + Directive, + ElementRef, + forwardRef, + inject, + input +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +@Directive({ + host: { + '(blur)': 'handleBlur()', + '(input)': 'handleInput()', + '[attr.inputmode]': '"decimal"', + '[attr.type]': '"text"' + }, + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GfLocalizedNumberDirective), + multi: true + } + ], + selector: 'input[gfLocalizedNumber]' +}) +export class GfLocalizedNumberDirective implements ControlValueAccessor { + public readonly locale = input(); + + private readonly document = inject(DOCUMENT); + private readonly elementRef = + inject>(ElementRef); + + public handleBlur() { + this.onTouched(); + } + + public handleInput() { + const value = this.elementRef.nativeElement.value; + + if (!value?.trim()) { + this.onChange(null); + return; + } + + // Locale resolution priority: + // 1. explicit [locale] input from the template + // 2. document.documentElement.lang — set by Angular i18n to the active + // language (e.g. 'de' when the app runs under /de/) + // 3. DEFAULT_LOCALE ('en-US') as the final fallback + const localeInput = this.locale(); + const documentLang = this.document.documentElement.lang; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const resolvedLocale = localeInput || documentLang || DEFAULT_LOCALE; + + const parsedNumber = extractNumberFromString({ + locale: resolvedLocale, + value + }); + + this.onChange( + parsedNumber !== undefined && !Number.isNaN(parsedNumber) + ? parsedNumber + : null + ); + } + + public registerOnChange(fn: (value: number | null) => void) { + this.onChange = fn; + } + + public registerOnTouched(fn: () => void) { + this.onTouched = fn; + } + + public setDisabledState(isDisabled: boolean) { + this.elementRef.nativeElement.disabled = isDisabled; + } + + public writeValue(value: number | null) { + if ( + value === null || + value === undefined || + typeof value !== 'number' || + Number.isNaN(value) + ) { + this.elementRef.nativeElement.value = ''; + return; + } + + const localeInput = this.locale(); + const documentLang = this.document.documentElement.lang; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const resolvedLocale = localeInput || documentLang || DEFAULT_LOCALE; + + this.elementRef.nativeElement.value = formatNumberForLocale({ + locale: resolvedLocale, + value + }); + } + + private onChange: (value: number | null) => void = () => undefined; + private onTouched: () => void = () => undefined; +}