Browse Source

fix: lookup search problems

pull/24516/head
sumeyye 1 month ago
parent
commit
0f79e2345f
  1. 80
      npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.html
  2. 9
      npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.scss
  3. 232
      npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.ts

80
npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.html

@ -1,45 +1,61 @@
<div class="abp-lookup-container position-relative">
@if (label()) {
<label class="form-label">{{ label() | abpLocalization }}</label>
<label class="form-label">{{ label() | abpLocalization }}</label>
}
<div class="input-group">
<input type="text" class="form-control" [placeholder]="placeholder() | abpLocalization" [ngModel]="displayValue()"
(ngModelChange)="onSearchInput($event)" (focus)="onSearchFocus()" (blur)="onSearchBlur($event)"
[disabled]="disabled()" />
<input
type="text"
class="form-control"
[placeholder]="placeholder() | abpLocalization"
[ngModel]="displayValue()"
(ngModelChange)="onSearchInput($event)"
(focus)="onSearchFocus()"
(blur)="onSearchBlur($event)"
[disabled]="disabled()"
/>
@if (displayValue() && !disabled()) {
<button type="button" class="btn btn-outline-secondary" (mousedown)="clearSelection()" tabindex="-1">
<i class="fa fa-times"></i>
</button>
<button
type="button"
class="btn btn-outline-secondary"
(mousedown)="clearSelection()"
tabindex="-1"
>
<i class="fa fa-times"></i>
</button>
}
</div>
@if (showDropdown() && !disabled()) {
<div class="abp-lookup-dropdown list-group position-absolute w-100 shadow">
@if (isLoading()) {
<div class="list-group-item text-center py-3">
<i class="fa fa-spinner fa-spin me-2"></i>
{{ 'AbpUi::Loading' | abpLocalization }}
</div>
} @else if (searchResults().length > 0) {
@for (item of searchResults(); track item.key) {
<button type="button" class="list-group-item list-group-item-action" (mousedown)="selectItem(item)">
@if (itemTemplate()) {
<ng-container *ngTemplateOutlet="itemTemplate()!; context: { $implicit: item }" />
} @else {
{{ getDisplayValue(item) }}
<div class="abp-lookup-dropdown list-group position-absolute w-100">
@if (isLoading()) {
<div class="list-group-item text-center py-3">
<i class="fa fa-spinner fa-spin me-2"></i>
{{ 'AbpUi::Loading' | abpLocalization }}
</div>
} @else if (searchResults().length > 0) {
@for (item of searchResults(); track item.key) {
<button
type="button"
class="list-group-item list-group-item-action"
(mousedown)="selectItem(item)"
>
@if (itemTemplate()) {
<ng-container *ngTemplateOutlet="itemTemplate()!; context: { $implicit: item }" />
} @else {
{{ getDisplayValue(item) }}
}
</button>
}
} @else if (displayValue()) {
@if (noResultsTemplate()) {
<ng-container *ngTemplateOutlet="noResultsTemplate()!" />
} @else {
<div class="list-group-item text-muted">
{{ 'AbpUi::NoDataAvailableInDatatable' | abpLocalization }}
</div>
}
}
</button>
}
} @else if (displayValue()) {
@if (noResultsTemplate()) {
<ng-container *ngTemplateOutlet="noResultsTemplate()!" />
} @else {
<div class="list-group-item text-muted">
{{ 'AbpUi::NoRecordsFound' | abpLocalization }}
</div>
}
}
</div>
}
</div>
</div>

9
npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.scss

@ -1,5 +1,8 @@
.abp-lookup-dropdown {
z-index: 1050;
max-height: 200px;
overflow-y: auto;
z-index: 1060;
max-height: 200px;
overflow-y: auto;
top: 100%;
margin-top: 0.25rem;
background-color: var(--lpx-content-bg);
}

232
npm/ng-packs/packages/components/lookup/src/lib/lookup-search.component.ts

@ -1,15 +1,15 @@
import {
Component,
input,
output,
model,
signal,
OnInit,
ChangeDetectionStrategy,
TemplateRef,
contentChild,
DestroyRef,
inject,
Component,
input,
output,
model,
signal,
OnInit,
ChangeDetectionStrategy,
TemplateRef,
contentChild,
DestroyRef,
inject,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CommonModule } from '@angular/common';
@ -18,119 +18,123 @@ import { LocalizationPipe } from '@abp/ng.core';
import { Subject, Observable, debounceTime, distinctUntilChanged, of, finalize } from 'rxjs';
export interface LookupItem {
key: string;
displayName: string;
[key: string]: unknown;
key: string;
displayName: string;
[key: string]: unknown;
}
export type LookupSearchFn<T = LookupItem> = (filter: string) => Observable<T[]>;
@Component({
selector: 'abp-lookup-search',
templateUrl: './lookup-search.component.html',
styleUrl: './lookup-search.component.scss',
imports: [CommonModule, FormsModule, LocalizationPipe],
changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'abp-lookup-search',
templateUrl: './lookup-search.component.html',
styleUrl: './lookup-search.component.scss',
imports: [CommonModule, FormsModule, LocalizationPipe],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class LookupSearchComponent<T extends LookupItem = LookupItem> implements OnInit {
private readonly destroyRef = inject(DestroyRef);
readonly label = input<string>();
readonly placeholder = input<string>('');
readonly debounceTime = input<number>(300);
readonly minSearchLength = input<number>(0);
readonly displayKey = input<keyof T>('displayName' as keyof T);
readonly valueKey = input<keyof T>('key' as keyof T);
readonly disabled = input<boolean>(false);
readonly searchFn = input<LookupSearchFn<T>>(() => of([]));
readonly selectedValue = model<string>('');
readonly displayValue = model<string>('');
readonly itemSelected = output<T>();
readonly searchChanged = output<string>();
readonly itemTemplate = contentChild<TemplateRef<{ $implicit: T }>>('itemTemplate');
readonly noResultsTemplate = contentChild<TemplateRef<void>>('noResultsTemplate');
readonly searchResults = signal<T[]>([]);
readonly showDropdown = signal(false);
readonly isLoading = signal(false);
private readonly searchSubject = new Subject<string>();
ngOnInit() {
this.searchSubject.pipe(
debounceTime(this.debounceTime()),
distinctUntilChanged(),
takeUntilDestroyed(this.destroyRef)
).subscribe(filter => {
this.performSearch(filter);
});
}
onSearchInput(filter: string) {
this.displayValue.set(filter);
this.showDropdown.set(true);
this.searchChanged.emit(filter);
if (filter.length >= this.minSearchLength()) {
this.searchSubject.next(filter);
} else {
this.searchResults.set([]);
}
}
onSearchFocus() {
this.showDropdown.set(true);
const currentFilter = this.displayValue() || '';
if (currentFilter.length >= this.minSearchLength()) {
this.performSearch(currentFilter);
}
}
onSearchBlur(event: FocusEvent) {
const relatedTarget = event.relatedTarget as HTMLElement;
if (!relatedTarget?.closest('.abp-lookup-dropdown')) {
this.showDropdown.set(false);
}
}
selectItem(item: T) {
const displayKeyValue = String(item[this.displayKey()] ?? '');
const valueKeyValue = String(item[this.valueKey()] ?? '');
this.displayValue.set(displayKeyValue);
this.selectedValue.set(valueKeyValue);
this.searchResults.set([]);
this.showDropdown.set(false);
this.itemSelected.emit(item);
}
clearSelection() {
this.displayValue.set('');
this.selectedValue.set('');
this.searchResults.set([]);
private readonly destroyRef = inject(DestroyRef);
readonly label = input<string>();
readonly placeholder = input<string>('');
readonly debounceTime = input<number>(300);
readonly minSearchLength = input<number>(0);
readonly displayKey = input<keyof T>('displayName' as keyof T);
readonly valueKey = input<keyof T>('key' as keyof T);
readonly disabled = input<boolean>(false);
readonly searchFn = input<LookupSearchFn<T>>(() => of([]));
readonly selectedValue = model<string>('');
readonly displayValue = model<string>('');
readonly itemSelected = output<T>();
readonly searchChanged = output<string>();
readonly itemTemplate = contentChild<TemplateRef<{ $implicit: T }>>('itemTemplate');
readonly noResultsTemplate = contentChild<TemplateRef<void>>('noResultsTemplate');
readonly searchResults = signal<T[]>([]);
readonly showDropdown = signal(true);
readonly isLoading = signal(false);
private readonly searchSubject = new Subject<string>();
ngOnInit() {
this.searchSubject
.pipe(
debounceTime(this.debounceTime()),
distinctUntilChanged(),
takeUntilDestroyed(this.destroyRef),
)
.subscribe(filter => {
this.performSearch(filter);
});
}
onSearchInput(filter: string) {
this.displayValue.set(filter);
this.showDropdown.set(true);
this.searchChanged.emit(filter);
if (filter.length >= this.minSearchLength()) {
this.searchSubject.next(filter);
} else {
this.searchResults.set([]);
}
}
private performSearch(filter: string) {
this.isLoading.set(true);
this.searchFn()(filter).pipe(
takeUntilDestroyed(this.destroyRef),
finalize(() => this.isLoading.set(false))
).subscribe({
next: results => {
this.searchResults.set(results);
},
error: () => {
this.searchResults.set([]);
}
});
onSearchFocus() {
this.showDropdown.set(true);
const currentFilter = this.displayValue() || '';
if (currentFilter.length >= this.minSearchLength()) {
this.performSearch(currentFilter);
}
}
getDisplayValue(item: T): string {
return String(item[this.displayKey()] ?? item[this.valueKey()] ?? '');
onSearchBlur(event: FocusEvent) {
const relatedTarget = event.relatedTarget as HTMLElement;
if (!relatedTarget?.closest('.abp-lookup-dropdown')) {
this.showDropdown.set(false);
}
}
selectItem(item: T) {
const displayKeyValue = String(item[this.displayKey()] ?? '');
const valueKeyValue = String(item[this.valueKey()] ?? '');
this.displayValue.set(displayKeyValue);
this.selectedValue.set(valueKeyValue);
this.searchResults.set([]);
this.showDropdown.set(false);
this.itemSelected.emit(item);
}
clearSelection() {
this.displayValue.set('');
this.selectedValue.set('');
this.searchResults.set([]);
}
private performSearch(filter: string) {
this.isLoading.set(true);
this.searchFn()(filter)
.pipe(
takeUntilDestroyed(this.destroyRef),
finalize(() => this.isLoading.set(false)),
)
.subscribe({
next: results => {
this.searchResults.set(results);
},
error: () => {
this.searchResults.set([]);
},
});
}
getDisplayValue(item: T): string {
return String(item[this.displayKey()] ?? item[this.valueKey()] ?? '');
}
}

Loading…
Cancel
Save