* Extract symbol search to component (#2003) * Update changelog
This commit is contained in:
parent
f5a50a95de
commit
fce3b2084e
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
- Extracted the symbol search to a dedicated component
|
||||||
- Upgraded `prisma` from version `4.14.1` to `4.15.0`
|
- Upgraded `prisma` from version `4.14.1` to `4.15.0`
|
||||||
|
|
||||||
## 1.280.1 - 2023-06-10
|
## 1.280.1 - 2023-06-10
|
||||||
|
@ -55,8 +55,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
public currencies: string[] = [];
|
public currencies: string[] = [];
|
||||||
public currentMarketPrice = null;
|
public currentMarketPrice = null;
|
||||||
public defaultDateFormat: string;
|
public defaultDateFormat: string;
|
||||||
public filteredLookupItems: LookupItem[] = [];
|
|
||||||
public filteredLookupItemsObservable: Observable<LookupItem[]> = of([]);
|
|
||||||
public filteredTagsObservable: Observable<Tag[]> = of([]);
|
public filteredTagsObservable: Observable<Tag[]> = of([]);
|
||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
public platforms: { id: string; name: string }[];
|
public platforms: { id: string; name: string }[];
|
||||||
@ -120,10 +118,12 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
name: [this.data.activity?.SymbolProfile?.name, Validators.required],
|
name: [this.data.activity?.SymbolProfile?.name, Validators.required],
|
||||||
quantity: [this.data.activity?.quantity, Validators.required],
|
quantity: [this.data.activity?.quantity, Validators.required],
|
||||||
searchSymbol: [
|
searchSymbol: [
|
||||||
{
|
!!this.data.activity?.SymbolProfile
|
||||||
dataSource: this.data.activity?.SymbolProfile?.dataSource,
|
? {
|
||||||
symbol: this.data.activity?.SymbolProfile?.symbol
|
dataSource: this.data.activity?.SymbolProfile?.dataSource,
|
||||||
},
|
symbol: this.data.activity?.SymbolProfile?.symbol
|
||||||
|
}
|
||||||
|
: null,
|
||||||
Validators.required
|
Validators.required
|
||||||
],
|
],
|
||||||
tags: [
|
tags: [
|
||||||
@ -238,28 +238,19 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.filteredLookupItemsObservable = this.activityForm.controls[
|
this.activityForm.controls['searchSymbol'].valueChanges.subscribe(() => {
|
||||||
'searchSymbol'
|
if (this.activityForm.controls['searchSymbol'].invalid) {
|
||||||
].valueChanges.pipe(
|
this.data.activity.SymbolProfile = null;
|
||||||
debounceTime(400),
|
} else {
|
||||||
distinctUntilChanged(),
|
this.activityForm.controls['dataSource'].setValue(
|
||||||
switchMap((query: string) => {
|
this.activityForm.controls['searchSymbol'].value.dataSource
|
||||||
if (isString(query) && query.length > 1) {
|
);
|
||||||
const filteredLookupItemsObservable =
|
|
||||||
this.dataService.fetchSymbols(query);
|
|
||||||
|
|
||||||
filteredLookupItemsObservable
|
this.updateSymbol();
|
||||||
.pipe(takeUntil(this.unsubscribeSubject))
|
}
|
||||||
.subscribe((filteredLookupItems) => {
|
|
||||||
this.filteredLookupItems = filteredLookupItems;
|
|
||||||
});
|
|
||||||
|
|
||||||
return filteredLookupItemsObservable;
|
this.changeDetectorRef.markForCheck();
|
||||||
}
|
});
|
||||||
|
|
||||||
return [];
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
this.filteredTagsObservable = this.activityForm.controls[
|
this.filteredTagsObservable = this.activityForm.controls[
|
||||||
'tags'
|
'tags'
|
||||||
@ -393,25 +384,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
this.tagInput.nativeElement.value = '';
|
this.tagInput.nativeElement.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
public onBlurSymbol() {
|
|
||||||
const currentLookupItem = this.filteredLookupItems.find((lookupItem) => {
|
|
||||||
return (
|
|
||||||
lookupItem.symbol ===
|
|
||||||
this.activityForm.controls['searchSymbol'].value.symbol
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (currentLookupItem) {
|
|
||||||
this.updateSymbol(currentLookupItem.symbol);
|
|
||||||
} else {
|
|
||||||
this.activityForm.controls['searchSymbol'].setErrors({ incorrect: true });
|
|
||||||
|
|
||||||
this.data.activity.SymbolProfile = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
|
||||||
}
|
|
||||||
|
|
||||||
public onCancel() {
|
public onCancel() {
|
||||||
this.dialogRef.close();
|
this.dialogRef.close();
|
||||||
}
|
}
|
||||||
@ -455,13 +427,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
this.dialogRef.close({ activity });
|
this.dialogRef.close({ activity });
|
||||||
}
|
}
|
||||||
|
|
||||||
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
|
|
||||||
this.activityForm.controls['dataSource'].setValue(
|
|
||||||
event.option.value.dataSource
|
|
||||||
);
|
|
||||||
this.updateSymbol(event.option.value.symbol);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ngOnDestroy() {
|
public ngOnDestroy() {
|
||||||
this.unsubscribeSubject.next();
|
this.unsubscribeSubject.next();
|
||||||
this.unsubscribeSubject.complete();
|
this.unsubscribeSubject.complete();
|
||||||
@ -477,12 +442,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateSymbol(symbol: string) {
|
private updateSymbol() {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
|
||||||
this.activityForm.controls['searchSymbol'].setErrors(null);
|
|
||||||
this.activityForm.controls['searchSymbol'].setValue({ symbol });
|
|
||||||
|
|
||||||
this.changeDetectorRef.markForCheck();
|
this.changeDetectorRef.markForCheck();
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
|
@ -48,34 +48,10 @@
|
|||||||
>
|
>
|
||||||
<mat-form-field appearance="outline" class="w-100">
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
<mat-label i18n>Name, symbol or ISIN</mat-label>
|
<mat-label i18n>Name, symbol or ISIN</mat-label>
|
||||||
<input
|
<gf-symbol-autocomplete
|
||||||
autocapitalize="off"
|
|
||||||
autocomplete="off"
|
|
||||||
autocorrect="off"
|
|
||||||
formControlName="searchSymbol"
|
formControlName="searchSymbol"
|
||||||
matInput
|
[isLoading]="isLoading"
|
||||||
[matAutocomplete]="symbolAutocomplete"
|
|
||||||
(blur)="onBlurSymbol()"
|
|
||||||
/>
|
/>
|
||||||
<mat-autocomplete
|
|
||||||
#symbolAutocomplete="matAutocomplete"
|
|
||||||
[displayWith]="displayFn"
|
|
||||||
(optionSelected)="onUpdateSymbol($event)"
|
|
||||||
>
|
|
||||||
<mat-option
|
|
||||||
*ngFor="let lookupItem of filteredLookupItemsObservable | async"
|
|
||||||
class="line-height-1"
|
|
||||||
[value]="lookupItem"
|
|
||||||
>
|
|
||||||
<span><b>{{ lookupItem.name }}</b></span>
|
|
||||||
<br />
|
|
||||||
<small class="text-muted"
|
|
||||||
>{{ lookupItem.symbol | gfSymbol }} · {{ lookupItem.currency
|
|
||||||
}}</small
|
|
||||||
>
|
|
||||||
</mat-option>
|
|
||||||
</mat-autocomplete>
|
|
||||||
<mat-spinner *ngIf="isLoading" matSuffix [diameter]="20"></mat-spinner>
|
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
@ -9,9 +9,8 @@ import { MatDatepickerModule } from '@angular/material/datepicker';
|
|||||||
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 { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import { MatSelectModule } from '@angular/material/select';
|
||||||
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
import { GfSymbolAutocompleteModule } from '@ghostfolio/ui/symbol-autocomplete/symbol-autocomplete.module';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
|
|
||||||
import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog.component';
|
import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog.component';
|
||||||
@ -21,7 +20,7 @@ import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FormsModule,
|
FormsModule,
|
||||||
GfSymbolModule,
|
GfSymbolAutocompleteModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
MatAutocompleteModule,
|
MatAutocompleteModule,
|
||||||
MatButtonModule,
|
MatButtonModule,
|
||||||
@ -31,7 +30,6 @@ import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog
|
|||||||
MatDialogModule,
|
MatDialogModule,
|
||||||
MatFormFieldModule,
|
MatFormFieldModule,
|
||||||
MatInputModule,
|
MatInputModule,
|
||||||
MatProgressSpinnerModule,
|
|
||||||
MatSelectModule,
|
MatSelectModule,
|
||||||
ReactiveFormsModule
|
ReactiveFormsModule
|
||||||
],
|
],
|
||||||
|
178
libs/ui/src/lib/symbol-autocomplete/abstract-mat-form-field.ts
Normal file
178
libs/ui/src/lib/symbol-autocomplete/abstract-mat-form-field.ts
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import { FocusMonitor } from '@angular/cdk/a11y';
|
||||||
|
import { coerceBooleanProperty } from '@angular/cdk/coercion';
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
DoCheck,
|
||||||
|
ElementRef,
|
||||||
|
HostBinding,
|
||||||
|
HostListener,
|
||||||
|
Input,
|
||||||
|
OnDestroy
|
||||||
|
} from '@angular/core';
|
||||||
|
import { ControlValueAccessor, NgControl } from '@angular/forms';
|
||||||
|
import { MatFormFieldControl } from '@angular/material/form-field';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
template: ''
|
||||||
|
})
|
||||||
|
export abstract class AbstractMatFormField<T>
|
||||||
|
implements ControlValueAccessor, DoCheck, MatFormFieldControl<T>, OnDestroy
|
||||||
|
{
|
||||||
|
@HostBinding()
|
||||||
|
public id = `${this.controlType}-${AbstractMatFormField.nextId++}`;
|
||||||
|
|
||||||
|
@HostBinding('attr.aria-describedBy') public describedBy = '';
|
||||||
|
|
||||||
|
public readonly autofilled: boolean;
|
||||||
|
public errorState: boolean;
|
||||||
|
public focused = false;
|
||||||
|
public readonly stateChanges = new Subject<void>();
|
||||||
|
public readonly userAriaDescribedBy: string;
|
||||||
|
|
||||||
|
protected onChange?: (value: T) => void;
|
||||||
|
protected onTouched?: () => void;
|
||||||
|
|
||||||
|
private static nextId: number = 0;
|
||||||
|
|
||||||
|
protected constructor(
|
||||||
|
protected _elementRef: ElementRef,
|
||||||
|
protected _focusMonitor: FocusMonitor,
|
||||||
|
public readonly ngControl: NgControl
|
||||||
|
) {
|
||||||
|
if (this.ngControl) {
|
||||||
|
this.ngControl.valueAccessor = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
_focusMonitor
|
||||||
|
.monitor(this._elementRef.nativeElement, true)
|
||||||
|
.subscribe((origin) => {
|
||||||
|
this.focused = !!origin;
|
||||||
|
this.stateChanges.next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _controlType: string;
|
||||||
|
|
||||||
|
public get controlType(): string {
|
||||||
|
return this._controlType;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected set controlType(value: string) {
|
||||||
|
this._controlType = value;
|
||||||
|
this.id = `${this._controlType}-${AbstractMatFormField.nextId++}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _value: T;
|
||||||
|
|
||||||
|
public get value(): T {
|
||||||
|
return this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set value(value: T) {
|
||||||
|
this._value = value;
|
||||||
|
|
||||||
|
if (this.onChange) {
|
||||||
|
this.onChange(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public get empty(): boolean {
|
||||||
|
return !this._value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public _placeholder: string = '';
|
||||||
|
|
||||||
|
public get placeholder() {
|
||||||
|
return this._placeholder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
public set placeholder(placeholder: string) {
|
||||||
|
this._placeholder = placeholder;
|
||||||
|
this.stateChanges.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
public _required: boolean = false;
|
||||||
|
|
||||||
|
public get required() {
|
||||||
|
return this._required;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
public set required(required: any) {
|
||||||
|
this._required = coerceBooleanProperty(required);
|
||||||
|
this.stateChanges.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
public _disabled: boolean = false;
|
||||||
|
|
||||||
|
public get disabled() {
|
||||||
|
if (this.ngControl && this.ngControl.disabled !== null) {
|
||||||
|
return this.ngControl.disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this._disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
public set disabled(disabled: any) {
|
||||||
|
this._disabled = coerceBooleanProperty(disabled);
|
||||||
|
|
||||||
|
if (this.focused) {
|
||||||
|
this.focused = false;
|
||||||
|
this.stateChanges.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract focus(): void;
|
||||||
|
|
||||||
|
public get shouldLabelFloat(): boolean {
|
||||||
|
return this.focused || !this.empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngDoCheck(): void {
|
||||||
|
if (this.ngControl) {
|
||||||
|
this.errorState = this.ngControl.invalid && this.ngControl.touched;
|
||||||
|
this.stateChanges.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy(): void {
|
||||||
|
this.stateChanges.complete();
|
||||||
|
this._focusMonitor.stopMonitoring(this._elementRef.nativeElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerOnChange(fn: (_: T) => void): void {
|
||||||
|
this.onChange = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public registerOnTouched(fn: () => void): void {
|
||||||
|
this.onTouched = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setDescribedByIds(ids: string[]): void {
|
||||||
|
this.describedBy = ids.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
public writeValue(value: T): void {
|
||||||
|
this.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@HostListener('focusout')
|
||||||
|
public onBlur() {
|
||||||
|
this.focused = false;
|
||||||
|
|
||||||
|
if (this.onTouched) {
|
||||||
|
this.onTouched();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stateChanges.next();
|
||||||
|
}
|
||||||
|
|
||||||
|
public onContainerClick(): void {
|
||||||
|
if (!this.focused) {
|
||||||
|
this.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
libs/ui/src/lib/symbol-autocomplete/index.ts
Normal file
1
libs/ui/src/lib/symbol-autocomplete/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './symbol-autocomplete.module';
|
@ -0,0 +1,34 @@
|
|||||||
|
<input
|
||||||
|
autocapitalize="off"
|
||||||
|
autocomplete="off"
|
||||||
|
matInput
|
||||||
|
[formControl]="control"
|
||||||
|
[matAutocomplete]="symbolAutocomplete"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<mat-autocomplete
|
||||||
|
#symbolAutocomplete="matAutocomplete"
|
||||||
|
[displayWith]="displayFn"
|
||||||
|
(optionSelected)="onUpdateSymbol($event)"
|
||||||
|
>
|
||||||
|
<ng-container *ngIf="!isLoading">
|
||||||
|
<mat-option
|
||||||
|
*ngFor="let lookupItem of filteredLookupItems"
|
||||||
|
class="line-height-1"
|
||||||
|
[value]="lookupItem"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
><b>{{ lookupItem.name }}</b></span
|
||||||
|
>
|
||||||
|
<br />
|
||||||
|
<small class="text-muted"
|
||||||
|
>{{ lookupItem.symbol | gfSymbol }} · {{ lookupItem.currency }}</small
|
||||||
|
>
|
||||||
|
</mat-option>
|
||||||
|
</ng-container>
|
||||||
|
</mat-autocomplete>
|
||||||
|
<mat-spinner
|
||||||
|
*ngIf="isLoading"
|
||||||
|
class="position-absolute"
|
||||||
|
[diameter]="20"
|
||||||
|
></mat-spinner>
|
@ -0,0 +1,8 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.mat-mdc-progress-spinner {
|
||||||
|
right: 0;
|
||||||
|
top: calc(50% - 10px);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,169 @@
|
|||||||
|
import { FocusMonitor } from '@angular/cdk/a11y';
|
||||||
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
ElementRef,
|
||||||
|
Input,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
import { FormControl, NgControl, Validators } from '@angular/forms';
|
||||||
|
import {
|
||||||
|
MatAutocomplete,
|
||||||
|
MatAutocompleteSelectedEvent
|
||||||
|
} from '@angular/material/autocomplete';
|
||||||
|
import { MatFormFieldControl } from '@angular/material/form-field';
|
||||||
|
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 { isString } from 'lodash';
|
||||||
|
import { Observable, Subject, of, tap } from 'rxjs';
|
||||||
|
import {
|
||||||
|
debounceTime,
|
||||||
|
distinctUntilChanged,
|
||||||
|
filter,
|
||||||
|
switchMap
|
||||||
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { AbstractMatFormField } from './abstract-mat-form-field';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
host: {
|
||||||
|
'[attr.aria-describedBy]': 'describedBy',
|
||||||
|
'[id]': 'id'
|
||||||
|
},
|
||||||
|
selector: 'gf-symbol-autocomplete',
|
||||||
|
styleUrls: ['./symbol-autocomplete.component.scss'],
|
||||||
|
templateUrl: 'symbol-autocomplete.component.html',
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: MatFormFieldControl,
|
||||||
|
useExisting: SymbolAutocompleteComponent
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class SymbolAutocompleteComponent
|
||||||
|
extends AbstractMatFormField<LookupItem>
|
||||||
|
implements OnInit, OnDestroy
|
||||||
|
{
|
||||||
|
@Input() public isLoading = false;
|
||||||
|
|
||||||
|
@ViewChild(MatInput, { static: false }) private input: MatInput;
|
||||||
|
|
||||||
|
@ViewChild('symbolAutocomplete') public symbolAutocomplete: MatAutocomplete;
|
||||||
|
|
||||||
|
public control = new FormControl();
|
||||||
|
public filteredLookupItems: LookupItem[] = [];
|
||||||
|
public filteredLookupItemsObservable: Observable<LookupItem[]> = of([]);
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
public readonly _elementRef: ElementRef,
|
||||||
|
public readonly _focusMonitor: FocusMonitor,
|
||||||
|
public readonly changeDetectorRef: ChangeDetectorRef,
|
||||||
|
public readonly dataService: DataService,
|
||||||
|
public readonly ngControl: NgControl
|
||||||
|
) {
|
||||||
|
super(_elementRef, _focusMonitor, ngControl);
|
||||||
|
|
||||||
|
this.controlType = 'symbol-autocomplete';
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnInit() {
|
||||||
|
super.required = this.ngControl.control?.hasValidator(Validators.required);
|
||||||
|
|
||||||
|
if (this.disabled) {
|
||||||
|
this.control.disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.control.valueChanges
|
||||||
|
.pipe(
|
||||||
|
debounceTime(400),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
filter((query) => {
|
||||||
|
return isString(query) && query.length > 1;
|
||||||
|
}),
|
||||||
|
tap(() => {
|
||||||
|
this.isLoading = true;
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
}),
|
||||||
|
switchMap((query: string) => {
|
||||||
|
return this.dataService.fetchSymbols(query);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.subscribe((filteredLookupItems) => {
|
||||||
|
this.filteredLookupItems = filteredLookupItems;
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public displayFn(aLookupItem: LookupItem) {
|
||||||
|
return aLookupItem?.symbol ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public get empty() {
|
||||||
|
return this.input?.empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public focus() {
|
||||||
|
this.input.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isValueInOptions(value: string) {
|
||||||
|
return this.filteredLookupItems.some((item) => {
|
||||||
|
return item.symbol === value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngDoCheck() {
|
||||||
|
if (this.ngControl) {
|
||||||
|
this.validateRequired();
|
||||||
|
this.validateSelection();
|
||||||
|
this.errorState = this.ngControl.invalid && this.ngControl.touched;
|
||||||
|
this.stateChanges.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
|
||||||
|
super.value = {
|
||||||
|
dataSource: event.option.value.dataSource,
|
||||||
|
symbol: event.option.value.symbol
|
||||||
|
} as LookupItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set value(value: LookupItem) {
|
||||||
|
this.control.setValue(value);
|
||||||
|
super.value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
super.ngOnDestroy();
|
||||||
|
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateRequired() {
|
||||||
|
const requiredCheck = super.required
|
||||||
|
? !super.value?.dataSource || !super.value?.symbol
|
||||||
|
: false;
|
||||||
|
if (requiredCheck) {
|
||||||
|
this.ngControl.control.setErrors({ invalidData: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateSelection() {
|
||||||
|
const error =
|
||||||
|
!this.isValueInOptions(this.input?.value) ||
|
||||||
|
this.input?.value !== super.value?.symbol;
|
||||||
|
if (error) {
|
||||||
|
this.ngControl.control.setErrors({ invalidData: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
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 { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||||
|
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
|
||||||
|
import { SymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete/symbol-autocomplete.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [SymbolAutocompleteComponent],
|
||||||
|
exports: [SymbolAutocompleteComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
GfSymbolModule,
|
||||||
|
MatAutocompleteModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
MatProgressSpinnerModule,
|
||||||
|
ReactiveFormsModule
|
||||||
|
],
|
||||||
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
|
})
|
||||||
|
export class GfSymbolAutocompleteModule {}
|
Loading…
x
Reference in New Issue
Block a user