Merge pull request #13090 from vvlladd28/improvement/string-item/fetch-option

Add fetch function for string items list; fix double item addition on blur
This commit is contained in:
Igor Kulikov 2025-04-02 15:19:19 +03:00 committed by GitHub
commit e27a2121b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 50 additions and 26 deletions

View File

@ -30,7 +30,7 @@
<mat-icon matChipRemove *ngIf="!disabled">close</mat-icon> <mat-icon matChipRemove *ngIf="!disabled">close</mat-icon>
</mat-chip-row> </mat-chip-row>
<input matInput type="text" <input matInput type="text"
(blur)="onTouched()" (blur)="addOnBlur($event)"
placeholder="{{ placeholder }}" placeholder="{{ placeholder }}"
style="max-width: 300px;min-width: 250px" style="max-width: 300px;min-width: 250px"
#stringItemInput #stringItemInput
@ -40,8 +40,7 @@
[matChipInputFor]="itemsChipList" [matChipInputFor]="itemsChipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" [matChipInputSeparatorKeyCodes]="separatorKeysCodes"
matAutocompleteOrigin matAutocompleteOrigin
matChipInputAddOnBlur (matChipInputTokenEnd)="addOnEnd($event)"
(matChipInputTokenEnd)="addItem($event)"
[matAutocompleteConnectedTo]="origin" [matAutocompleteConnectedTo]="origin"
[matAutocomplete]="stringItemAutocomplete" [matAutocomplete]="stringItemAutocomplete"
[matAutocompleteDisabled]="!predefinedValues?.length"> [matAutocompleteDisabled]="!predefinedValues?.length">
@ -49,12 +48,11 @@
<mat-autocomplete #stringItemAutocomplete="matAutocomplete" <mat-autocomplete #stringItemAutocomplete="matAutocomplete"
[displayWith]="displayValueFn" [displayWith]="displayValueFn"
class="tb-autocomplete"> class="tb-autocomplete">
<mat-option *ngFor="let value of filteredValues | async" [value]="value"> @for (value of filteredValues | async; track value.value) {
<span [innerHTML]="value.name | highlight:searchText"></span> <mat-option [value]="value"><span [innerHTML]="value.name | highlight:searchText"></span></mat-option>
</mat-option> } @empty {
<mat-option *ngIf="!(filteredValues | async)?.length" [value]="null"> <mat-option [value]="null">{{ 'common.not-found' | translate }}</mat-option>
{{ 'common.not-found' | translate }} }
</mat-option>
</mat-autocomplete> </mat-autocomplete>
<mat-hint [hidden]="!hint"> <mat-hint [hidden]="!hint">
{{ hint }} {{ hint }}

View File

@ -30,6 +30,7 @@ import { coerceArray, coerceBoolean } from '@shared/decorators/coercion';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { filter, mergeMap, share, tap } from 'rxjs/operators'; import { filter, mergeMap, share, tap } from 'rxjs/operators';
import { MatAutocompleteTrigger } from '@angular/material/autocomplete'; import { MatAutocompleteTrigger } from '@angular/material/autocomplete';
import { isDefined, isUndefined } from '@core/utils';
export interface StringItemsOption { export interface StringItemsOption {
name: string; name: string;
@ -116,6 +117,13 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit {
@coerceArray() @coerceArray()
predefinedValues: StringItemsOption[]; predefinedValues: StringItemsOption[];
@Input()
fetchOptionsFn: (searchText?: string) => Observable<Array<StringItemsOption>>;
@Input()
@coerceBoolean()
allowUserValue = false;
get itemsControl(): AbstractControl { get itemsControl(): AbstractControl {
return this.stringItemsForm.get('items'); return this.stringItemsForm.get('items');
} }
@ -124,7 +132,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit {
return this.stringItemsForm.get('item'); return this.stringItemsForm.get('item');
} }
onTouched = () => {}; private onTouched = () => {};
private propagateChange: (value: any) => void = () => {}; private propagateChange: (value: any) => void = () => {};
private dirty = false; private dirty = false;
@ -136,7 +144,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit {
} }
ngOnInit() { ngOnInit() {
if (this.predefinedValues) { if (this.predefinedValues || isDefined(this.fetchOptionsFn)) {
this.filteredValues = this.itemControl.valueChanges this.filteredValues = this.itemControl.valueChanges
.pipe( .pipe(
tap((value) => { tap((value) => {
@ -147,7 +155,8 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit {
} }
}), }),
filter((value) => typeof value === 'string'), filter((value) => typeof value === 'string'),
mergeMap(name => this.fetchValues(name)), tap(name => this.searchText = name),
mergeMap(name => this.fetchOptionsFn ? this.fetchOptionsFn(name) : this.fetchValues(name)),
share() share()
); );
} }
@ -180,7 +189,7 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit {
if (value != null && value.length > 0) { if (value != null && value.length > 0) {
this.modelValue = [...value]; this.modelValue = [...value];
this.itemList = []; this.itemList = [];
if (this.predefinedValues) { if (this.predefinedValues && !this.allowUserValue) {
value.forEach(item => { value.forEach(item => {
const findItem = this.predefinedValues.find(option => option.value === item); const findItem = this.predefinedValues.find(option => option.value === item);
if (findItem) { if (findItem) {
@ -199,19 +208,16 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit {
this.dirty = true; this.dirty = true;
} }
addItem(event: MatChipInputEvent): void { addOnBlur(event: FocusEvent) {
const item = event.value?.trim() ?? ''; const target: HTMLElement = event.relatedTarget as HTMLElement;
if (item) { if (target && target.tagName !== 'MAT-OPTION') {
if (this.predefinedValues) { this.addItem(this.stringItemInput.nativeElement.value ?? '')
const findItems = this.predefinedValues
.filter(value => value.name.toLowerCase().includes(item.toLowerCase()));
if (findItems.length === 1) {
this.add(findItems[0]);
}
} else {
this.add({value: item, name: item});
}
} }
this.onTouched();
}
addOnEnd(event: MatChipInputEvent): void {
this.addItem(event.value ?? '')
} }
removeItems(item: StringItemsOption) { removeItems(item: StringItemsOption) {
@ -239,6 +245,27 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit {
return values ? values.name : undefined; return values ? values.name : undefined;
} }
private addItem(searchText: string) {
searchText = searchText.trim();
if (searchText) {
if (this.allowUserValue || !this.predefinedValues && isUndefined(this.fetchOptionsFn)) {
this.add({value: searchText, name: searchText});
} else if (this.predefinedValues) {
const findItems = this.predefinedValues
.filter(value => value.name.toLowerCase().includes(searchText.toLowerCase()));
if (findItems.length === 1) {
this.add(findItems[0]);
}
} else if (isDefined(this.fetchOptionsFn)) {
this.fetchOptionsFn(searchText).subscribe((findItems) => {
if (findItems.length === 1) {
this.add(findItems[0]);
}
})
}
}
}
private add(item: StringItemsOption) { private add(item: StringItemsOption) {
if (!this.modelValue || this.modelValue.indexOf(item.value) === -1) { if (!this.modelValue || this.modelValue.indexOf(item.value) === -1) {
if (!this.modelValue) { if (!this.modelValue) {
@ -256,7 +283,6 @@ export class StringItemsListComponent implements ControlValueAccessor, OnInit {
if (!this.predefinedValues?.length) { if (!this.predefinedValues?.length) {
return of([]); return of([]);
} }
this.searchText = searchText;
let result = this.predefinedValues; let result = this.predefinedValues;
if (searchText && searchText.length) { if (searchText && searchText.length) {
result = this.predefinedValues.filter(option => option.name.toLowerCase().includes(searchText.toLowerCase())); result = this.predefinedValues.filter(option => option.name.toLowerCase().includes(searchText.toLowerCase()));