Create reusable currency selector component using mat-autocomplete (#2487)
* Create reusable currency selector component using mat-autocomplete * Update changelog
This commit is contained in:
parent
c4e8e37884
commit
96b5dcfaf8
CHANGELOG.md
apps/client/src/app/pages/accounts/create-or-update-account-dialog
create-or-update-account-dialog.component.tscreate-or-update-account-dialog.htmlcreate-or-update-account-dialog.module.ts
libs
common/src/lib/interfaces
ui/src/lib
@ -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/),
|
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).
|
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
|
## 2.14.0 - 2023-10-21
|
||||||
|
|
||||||
### Added
|
### 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 { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto';
|
||||||
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
|
import { Currency } from '@ghostfolio/common/interfaces/currency.interface';
|
||||||
import { Platform } from '@prisma/client';
|
import { Platform } from '@prisma/client';
|
||||||
import { Observable, Subject } from 'rxjs';
|
import { Observable, Subject } from 'rxjs';
|
||||||
import { map, startWith } from 'rxjs/operators';
|
import { map, startWith } from 'rxjs/operators';
|
||||||
@ -30,7 +31,7 @@ import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces';
|
|||||||
})
|
})
|
||||||
export class CreateOrUpdateAccountDialog implements OnDestroy {
|
export class CreateOrUpdateAccountDialog implements OnDestroy {
|
||||||
public accountForm: FormGroup;
|
public accountForm: FormGroup;
|
||||||
public currencies: string[] = [];
|
public currencies: Currency[] = [];
|
||||||
public filteredPlatforms: Observable<Platform[]>;
|
public filteredPlatforms: Observable<Platform[]>;
|
||||||
public platforms: Platform[];
|
public platforms: Platform[];
|
||||||
|
|
||||||
@ -46,7 +47,10 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
|||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
const { currencies, platforms } = this.dataService.fetchInfo();
|
const { currencies, platforms } = this.dataService.fetchInfo();
|
||||||
|
|
||||||
this.currencies = currencies;
|
this.currencies = currencies.map((currency) => ({
|
||||||
|
label: currency,
|
||||||
|
value: currency
|
||||||
|
}));
|
||||||
this.platforms = platforms;
|
this.platforms = platforms;
|
||||||
|
|
||||||
this.accountForm = this.formBuilder.group({
|
this.accountForm = this.formBuilder.group({
|
||||||
@ -101,7 +105,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy {
|
|||||||
const account: CreateAccountDto | UpdateAccountDto = {
|
const account: CreateAccountDto | UpdateAccountDto = {
|
||||||
balance: this.accountForm.controls['balance'].value,
|
balance: this.accountForm.controls['balance'].value,
|
||||||
comment: this.accountForm.controls['comment'].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,
|
id: this.accountForm.controls['accountId'].value,
|
||||||
isExcluded: this.accountForm.controls['isExcluded'].value,
|
isExcluded: this.accountForm.controls['isExcluded'].value,
|
||||||
name: this.accountForm.controls['name'].value,
|
name: this.accountForm.controls['name'].value,
|
||||||
|
@ -20,11 +20,10 @@
|
|||||||
<div>
|
<div>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Currency</mat-label>
|
<mat-label i18n>Currency</mat-label>
|
||||||
<mat-select formControlName="currency">
|
<gf-currency-selector
|
||||||
<mat-option *ngFor="let currency of currencies" [value]="currency"
|
formControlName="currency"
|
||||||
>{{ currency }}</mat-option
|
[currencies]="currencies"
|
||||||
>
|
/>
|
||||||
</mat-select>
|
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -37,7 +36,7 @@
|
|||||||
(keydown.enter)="$event.stopPropagation()"
|
(keydown.enter)="$event.stopPropagation()"
|
||||||
/>
|
/>
|
||||||
<span class="ml-2" matTextSuffix
|
<span class="ml-2" matTextSuffix
|
||||||
>{{ accountForm.controls['currency'].value }}</span
|
>{{ accountForm.controls['currency']?.value?.value }}</span
|
||||||
>
|
>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,8 +7,8 @@ import { MatCheckboxModule } from '@angular/material/checkbox';
|
|||||||
import { MatDialogModule } from '@angular/material/dialog';
|
import { MatDialogModule } from '@angular/material/dialog';
|
||||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
import { MatInputModule } from '@angular/material/input';
|
import { MatInputModule } from '@angular/material/input';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
|
||||||
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
|
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';
|
import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.component';
|
||||||
|
|
||||||
@ -17,6 +17,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
|
GfCurrencySelectorModule,
|
||||||
GfSymbolIconModule,
|
GfSymbolIconModule,
|
||||||
MatAutocompleteModule,
|
MatAutocompleteModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
@ -24,7 +25,6 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c
|
|||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatSelectModule,
|
|
||||||
ReactiveFormsModule
|
ReactiveFormsModule
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
4
libs/common/src/lib/interfaces/currency.interface.ts
Normal file
4
libs/common/src/lib/interfaces/currency.interface.ts
Normal file
@ -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,3 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
167
libs/ui/src/lib/currency-selector/currency-selector.component.ts
Normal file
167
libs/ui/src/lib/currency-selector/currency-selector.component.ts
Normal file
@ -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 { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
import { DataService } from '@ghostfolio/client/services/data.service';
|
||||||
import { translate } from '@ghostfolio/ui/i18n';
|
import { translate } from '@ghostfolio/ui/i18n';
|
||||||
|
import { AbstractMatFormField } from '@ghostfolio/ui/shared/abstract-mat-form-field';
|
||||||
import { isString } from 'lodash';
|
import { isString } from 'lodash';
|
||||||
import { Subject, tap } from 'rxjs';
|
import { Subject, tap } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
@ -29,8 +30,6 @@ import {
|
|||||||
takeUntil
|
takeUntil
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { AbstractMatFormField } from './abstract-mat-form-field';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
host: {
|
host: {
|
||||||
@ -54,7 +53,7 @@ export class SymbolAutocompleteComponent
|
|||||||
@Input() private includeIndices = false;
|
@Input() private includeIndices = false;
|
||||||
@Input() public isLoading = false;
|
@Input() public isLoading = false;
|
||||||
|
|
||||||
@ViewChild(MatInput, { static: false }) private input: MatInput;
|
@ViewChild(MatInput) private input: MatInput;
|
||||||
|
|
||||||
@ViewChild('symbolAutocomplete') public symbolAutocomplete: MatAutocomplete;
|
@ViewChild('symbolAutocomplete') public symbolAutocomplete: MatAutocomplete;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user