Create reusable currency selector component using mat-autocomplete (#2487)

* Create reusable currency selector component using mat-autocomplete

* Update changelog
pull/2534/head^2
Aldrin 11 months ago committed by GitHub
parent c4e8e37884
commit 96b5dcfaf8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased
### Changed
- Changed the currency selector in the create or update account dialog to `@angular/material/autocomplete`
## 2.14.0 - 2023-10-21
### Added

@ -15,6 +15,7 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
import { DataService } from '@ghostfolio/client/services/data.service';
import { Currency } from '@ghostfolio/common/interfaces/currency.interface';
import { Platform } from '@prisma/client';
import { Observable, Subject } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
@ -30,7 +31,7 @@ import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces';
})
export class CreateOrUpdateAccountDialog implements OnDestroy {
public accountForm: FormGroup;
public currencies: string[] = [];
public currencies: Currency[] = [];
public filteredPlatforms: Observable<Platform[]>;
public platforms: Platform[];
@ -46,7 +47,10 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
public ngOnInit() {
const { currencies, platforms } = this.dataService.fetchInfo();
this.currencies = currencies;
this.currencies = currencies.map((currency) => ({
label: currency,
value: currency
}));
this.platforms = platforms;
this.accountForm = this.formBuilder.group({
@ -101,7 +105,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
const account: CreateAccountDto | UpdateAccountDto = {
balance: this.accountForm.controls['balance'].value,
comment: this.accountForm.controls['comment'].value,
currency: this.accountForm.controls['currency'].value,
currency: this.accountForm.controls['currency'].value?.value,
id: this.accountForm.controls['accountId'].value,
isExcluded: this.accountForm.controls['isExcluded'].value,
name: this.accountForm.controls['name'].value,

@ -20,11 +20,10 @@
<div>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Currency</mat-label>
<mat-select formControlName="currency">
<mat-option *ngFor="let currency of currencies" [value]="currency"
>{{ currency }}</mat-option
>
</mat-select>
<gf-currency-selector
formControlName="currency"
[currencies]="currencies"
/>
</mat-form-field>
</div>
<div>
@ -37,7 +36,7 @@
(keydown.enter)="$event.stopPropagation()"
/>
<span class="ml-2" matTextSuffix
>{{ accountForm.controls['currency'].value }}</span
>{{ accountForm.controls['currency']?.value?.value }}</span
>
</mat-form-field>
</div>

@ -7,8 +7,8 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
import { GfCurrencySelectorModule } from '@ghostfolio/ui/currency-selector/currency-selector.module';
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.component';
@ -17,6 +17,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
imports: [
CommonModule,
FormsModule,
GfCurrencySelectorModule,
GfSymbolIconModule,
MatAutocompleteModule,
MatButtonModule,
@ -24,7 +25,6 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
ReactiveFormsModule
]
})

@ -0,0 +1,4 @@
export interface Currency {
label: string;
value: string;
}

@ -0,0 +1,21 @@
<input
autocapitalize="off"
autocomplete="off"
matInput
[formControl]="control"
[matAutocomplete]="currencyAutocomplete"
/>
<mat-autocomplete
#currencyAutocomplete="matAutocomplete"
[displayWith]="displayFn"
(optionSelected)="onUpdateCurrency($event)"
>
<mat-option
*ngFor="let currencyItem of filteredCurrencies"
class="line-height-1"
[value]="currencyItem"
>
{{ currencyItem.label }}
</mat-option>
</mat-autocomplete>

@ -0,0 +1,167 @@
import { FocusMonitor } from '@angular/cdk/a11y';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
import { FormControl, FormGroupDirective, NgControl } from '@angular/forms';
import {
MatAutocomplete,
MatAutocompleteSelectedEvent
} from '@angular/material/autocomplete';
import { MatFormFieldControl } from '@angular/material/form-field';
import { MatInput } from '@angular/material/input';
import { Currency } from '@ghostfolio/common/interfaces/currency.interface';
import { AbstractMatFormField } from '@ghostfolio/ui/shared/abstract-mat-form-field';
import { Subject } from 'rxjs';
import { map, startWith, takeUntil } from 'rxjs/operators';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[attr.aria-describedBy]': 'describedBy',
'[id]': 'id'
},
providers: [
{
provide: MatFormFieldControl,
useExisting: CurrencySelectorComponent
}
],
selector: 'gf-currency-selector',
styleUrls: ['./currency-selector.component.scss'],
templateUrl: 'currency-selector.component.html'
})
export class CurrencySelectorComponent
extends AbstractMatFormField<Currency>
implements OnInit, OnDestroy
{
@Input() private currencies: Currency[] = [];
@Input() private formControlName: string;
@ViewChild(MatInput) private input: MatInput;
@ViewChild('currencyAutocomplete')
public currencyAutocomplete: MatAutocomplete;
public control = new FormControl();
public filteredCurrencies: Currency[] = [];
private unsubscribeSubject = new Subject<void>();
public constructor(
public readonly _elementRef: ElementRef,
public readonly _focusMonitor: FocusMonitor,
public readonly changeDetectorRef: ChangeDetectorRef,
private readonly formGroupDirective: FormGroupDirective,
public readonly ngControl: NgControl
) {
super(_elementRef, _focusMonitor, ngControl);
this.controlType = 'currency-selector';
}
public ngOnInit() {
if (this.disabled) {
this.control.disable();
}
const formGroup = this.formGroupDirective.form;
if (formGroup) {
const control = formGroup.get(this.formControlName);
if (control) {
this.value = this.currencies.find(({ value }) => {
return value === control.value;
});
}
}
this.control.valueChanges
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
if (super.value?.value) {
super.value.value = null;
}
});
this.control.valueChanges
.pipe(
takeUntil(this.unsubscribeSubject),
startWith(''),
map((value) => {
return value ? this.filter(value) : this.currencies.slice();
})
)
.subscribe((values) => {
this.filteredCurrencies = values;
});
}
public displayFn(currency: Currency) {
return currency?.label ?? '';
}
public get empty() {
return this.input?.empty;
}
public focus() {
this.input.focus();
}
public ngDoCheck() {
if (this.ngControl) {
this.validateRequired();
this.errorState = this.ngControl.invalid && this.ngControl.touched;
this.stateChanges.next();
}
}
public onUpdateCurrency(event: MatAutocompleteSelectedEvent) {
super.value = {
label: event.option.value.label,
value: event.option.value.value
} as Currency;
}
public set value(value: Currency) {
const newValue =
typeof value === 'object' && value !== null ? { ...value } : value;
this.control.setValue(newValue);
super.value = newValue;
}
public ngOnDestroy() {
super.ngOnDestroy();
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private filter(value: Currency | string) {
const filterValue =
typeof value === 'string'
? value?.toLowerCase()
: value?.value.toLowerCase();
return this.currencies.filter((currency) => {
return currency.value.toLowerCase().startsWith(filterValue);
});
}
private validateRequired() {
const requiredCheck = super.required
? !super.value.label || !super.value.value
: false;
if (requiredCheck) {
this.ngControl.control.setErrors({ invalidData: true });
}
}
}

@ -0,0 +1,23 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { CurrencySelectorComponent } from './currency-selector.component';
@NgModule({
declarations: [CurrencySelectorComponent],
exports: [CurrencySelectorComponent],
imports: [
CommonModule,
FormsModule,
MatAutocompleteModule,
MatFormFieldModule,
MatInputModule,
ReactiveFormsModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfCurrencySelectorModule {}

@ -19,6 +19,7 @@ import { MatInput } from '@angular/material/input';
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { DataService } from '@ghostfolio/client/services/data.service';
import { translate } from '@ghostfolio/ui/i18n';
import { AbstractMatFormField } from '@ghostfolio/ui/shared/abstract-mat-form-field';
import { isString } from 'lodash';
import { Subject, tap } from 'rxjs';
import {
@ -29,8 +30,6 @@ import {
takeUntil
} from 'rxjs/operators';
import { AbstractMatFormField } from './abstract-mat-form-field';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
@ -54,7 +53,7 @@ export class SymbolAutocompleteComponent
@Input() private includeIndices = false;
@Input() public isLoading = false;
@ViewChild(MatInput, { static: false }) private input: MatInput;
@ViewChild(MatInput) private input: MatInput;
@ViewChild('symbolAutocomplete') public symbolAutocomplete: MatAutocomplete;

Loading…
Cancel
Save