import {Component, OnInit, Input, Output, ViewChild, EventEmitter, ElementRef} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {map} from 'rxjs/operators/map';
+import {tap} from 'rxjs/operators/tap';
+import {reduce} from 'rxjs/operators/reduce';
+import 'rxjs/add/observable/of';
+import {mergeMap} from 'rxjs/operators/mergeMap';
import {mapTo} from 'rxjs/operators/mapTo';
import {debounceTime} from 'rxjs/operators/debounceTime';
import {distinctUntilChanged} from 'rxjs/operators/distinctUntilChanged';
@Input() persistKey: string; // TODO
- // Display all entries when the user clicks in the text filter
- // box regardless of any text that already exists there.
- @Input() clickShowsAll = true;
-
@Input() allowFreeText = false;
// Entry ID of the default entry to select (optional)
// onChange() is NOT fired when applying the default value
@Input() startId: any;
+ @Input() asyncDataSource: (term: string) => Observable<ComboboxEntry>;
+
+ // Useful for efficiently preventing duplicate async entries
+ asyncIds: {[idx: string]: boolean};
+
// True if a default selection has been made.
defaultSelectionApplied: boolean;
private store: StoreService,
) {
this.entrylist = [];
+ this.asyncIds = {};
this.click$ = new Subject<string>();
this.onChange = new EventEmitter<ComboboxEntry>();
this.freeTextId = -1;
this.onChange.emit(selEvent.item);
}
+ // Adds matching async entries to the entry list
+ // and propagates the search term for pipelining.
+ addAsyncEntries(term: string): Observable<string> {
+
+ if (!term || !this.asyncDataSource) {
+ return Observable.of(term);
+ }
+
+ return new Observable(observer => {
+ this.asyncDataSource(term).subscribe(
+ (entry: ComboboxEntry) => {
+ if (!this.asyncIds[''+entry.id]) {
+ this.asyncIds[''+entry.id] = true;
+ this.addEntry(entry);
+ }
+ },
+ err => {},
+ () => {
+ observer.next(term);
+ observer.complete();
+ }
+ )
+ });
+ }
+
filter = (text$: Observable<string>): Observable<ComboboxEntry[]> => {
return text$.pipe(
debounceTime(200),
distinctUntilChanged(),
+ // Merge click actions in with the stream of text entry
merge(
// Inject a specifier indicating the source of the
// action is a user click instead of a text entry.
- this.click$
- .pipe(filter(() => !this.instance.isPopupOpen()))
- .pipe(map(nothing => {
- if (this.clickShowsAll) {
- return '_CLICK_';
- } else {
- return nothing;
- }
- }))
+ // This tells the filter to show all values in sync mode.
+ this.click$.pipe(filter(() =>
+ !this.instance.isPopupOpen() && !this.asyncDataSource
+ )).pipe(mapTo('_CLICK_'))
),
- map(term => {
+ // mergeMap coalesces an observable into our stream.
+ mergeMap(term => this.addAsyncEntries(term)),
+ map((term: string) => {
+
if (term === '' || term === '_CLICK_') {
- // Click events display all visible entrylist
- return this.entrylist;
+ if (this.asyncDataSource) {
+ return [];
+ } else {
+ // In sync mode, a post-focus empty search or
+ // click event displays the whole list.
+ return this.entrylist;
+ }
}
// Filter entrylist whose labels substring-match the
</eg-progress-dialog>
<button class="btn btn-light" (click)="showProgress()">Test Progress Dialog</button>
</div>
- <div class="col-lg-4">
- <button class="btn btn-info" (click)="testToast()">Test Toast Message</button>
+ <div class="col-lg-3">
+ <eg-combobox [allowFreeText]="true"
+ placeholder="Combobox with static data"
+ [entries]="cbEntries"></eg-combobox>
</div>
- <div class="col-lg-4">
+ <div class="col-lg-3">
<eg-combobox [allowFreeText]="true"
- [entries]="taEntries" placeholder="Combobox..."></eg-combobox>
+ placeholder="Combobox with dynamic data"
+ [asyncDataSource]="cbAsyncSource"></eg-combobox>
+ </div>
+</div>
+<div class="row mb-3">
+ <div class="col-lg-4">
+ <button class="btn btn-info" (click)="testToast()">Test Toast Message</button>
</div>
</div>
<!-- /Progress Dialog Experiments ----------------------------- -->
import {StringService} from '@eg/share/string/string.service';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/observable/timer';
+import 'rxjs/add/observable/of';
import {map} from 'rxjs/operators/map';
import {take} from 'rxjs/operators/take';
import {GridDataSource, GridColumn, GridRowFlairEntry} from '@eg/share/grid/grid';
gridDataSource: GridDataSource = new GridDataSource();
- taEntries: ComboboxEntry[];
+ cbEntries: ComboboxEntry[];
+ // supplier of async combobox data
+ cbAsyncSource: (term: string) => Observable<ComboboxEntry>;
btSource: GridDataSource = new GridDataSource();
world = 'world'; // for local template version
this.pcrud.retrieveAll('cmrcfld', {order_by:{cmrcfld: 'name'}})
.subscribe(format => {
- if (!this.taEntries) { this.taEntries = []; }
- this.taEntries.push({id: format.id(), label: format.name()})
+ if (!this.cbEntries) { this.cbEntries = []; }
+ this.cbEntries.push({id: format.id(), label: format.name()})
});
+ this.cbAsyncSource = term => {
+ return this.pcrud.search(
+ 'cmrcfld',
+ {name: {'ilike': `%${term}%`}}, // could -or search on label
+ {order_by: {cmrcfld: 'name'}}
+ ).pipe(map(marcField => {
+ return {id: marcField.id(), label: marcField.name()};
+ }));
+ }
+
this.btSource.getRows = (pager: Pager, sort: any[]) => {
const orderBy: any = {cbt: 'name'};