From ff338f5753c9594258da987edbbfd4445d17e0f7 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Tue, 3 Jul 2018 12:12:14 -0400 Subject: [PATCH] LP#1775466 combobox gets dynamic data Signed-off-by: Bill Erickson --- .../src/app/share/combobox/combobox.component.ts | 67 ++++++++++++++++------ .../src/app/staff/sandbox/sandbox.component.html | 16 ++++-- .../eg2/src/app/staff/sandbox/sandbox.component.ts | 19 +++++- 3 files changed, 79 insertions(+), 23 deletions(-) diff --git a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts index 470ef0d2a6..47fedf9efb 100644 --- a/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts +++ b/Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts @@ -6,6 +6,10 @@ 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'; @@ -43,16 +47,17 @@ export class ComboboxComponent implements OnInit { @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; + + // Useful for efficiently preventing duplicate async entries + asyncIds: {[idx: string]: boolean}; + // True if a default selection has been made. defaultSelectionApplied: boolean; @@ -73,6 +78,7 @@ export class ComboboxComponent implements OnInit { private store: StoreService, ) { this.entrylist = []; + this.asyncIds = {}; this.click$ = new Subject(); this.onChange = new EventEmitter(); this.freeTextId = -1; @@ -144,29 +150,58 @@ export class ComboboxComponent implements OnInit { this.onChange.emit(selEvent.item); } + // Adds matching async entries to the entry list + // and propagates the search term for pipelining. + addAsyncEntries(term: string): Observable { + + 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): Observable => { 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 diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html index 0c0a66532d..dbcd3062c0 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html @@ -43,12 +43,20 @@ -
- +
+
-
+
+ placeholder="Combobox with dynamic data" + [asyncDataSource]="cbAsyncSource"> +
+
+
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts index 9151b1fcde..c415f8f509 100644 --- a/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts @@ -4,6 +4,7 @@ import {ToastService} from '@eg/share/toast/toast.service'; 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'; @@ -33,7 +34,9 @@ export class SandboxComponent implements OnInit { gridDataSource: GridDataSource = new GridDataSource(); - taEntries: ComboboxEntry[]; + cbEntries: ComboboxEntry[]; + // supplier of async combobox data + cbAsyncSource: (term: string) => Observable; btSource: GridDataSource = new GridDataSource(); world = 'world'; // for local template version @@ -70,10 +73,20 @@ export class SandboxComponent implements OnInit { 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'}; -- 2.11.0