LP#1775466 Generic typeahead component
authorBill Erickson <berickxx@gmail.com>
Fri, 29 Jun 2018 19:56:20 +0000 (15:56 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 5 Sep 2018 14:05:23 +0000 (10:05 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/share/org-select/org-select.component.ts
Open-ILS/src/eg2/src/app/share/typeahead/typeahead.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/share/typeahead/typeahead.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/common.module.ts
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.html
Open-ILS/src/eg2/src/app/staff/sandbox/sandbox.component.ts

index 8055616..99ea398 100644 (file)
@@ -1,3 +1,4 @@
+/** TODO PORT ME TO <eg-typeahead> */
 import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
 import {Observable} from 'rxjs/Observable';
 import {map} from 'rxjs/operators/map';
diff --git a/Open-ILS/src/eg2/src/app/share/typeahead/typeahead.component.html b/Open-ILS/src/eg2/src/app/share/typeahead/typeahead.component.html
new file mode 100644 (file)
index 0000000..219b2d3
--- /dev/null
@@ -0,0 +1,17 @@
+
+<!-- todo disabled -->
+<ng-template #displayTemplate let-r="result">
+{{r.label}}
+</ng-template>
+
+<input type="text" 
+  class="form-control"
+  [placeholder]="placeholder"
+  [(ngModel)]="selected" 
+  [ngbTypeahead]="filter"
+  [resultTemplate]="displayTemplate"
+  [inputFormatter]="formatMatchStrin"
+  (click)="click$.next($event.target.value)"
+  (selectItem)="selectorChanged($event)"
+  #instance="ngbTypeahead"
+/>
diff --git a/Open-ILS/src/eg2/src/app/share/typeahead/typeahead.component.ts b/Open-ILS/src/eg2/src/app/share/typeahead/typeahead.component.ts
new file mode 100644 (file)
index 0000000..0aad3d7
--- /dev/null
@@ -0,0 +1,118 @@
+import {Component, OnInit, Input, Output, ViewChild, EventEmitter} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {map} from 'rxjs/operators/map';
+import {mapTo} from 'rxjs/operators/mapTo';
+import {debounceTime} from 'rxjs/operators/debounceTime';
+import {distinctUntilChanged} from 'rxjs/operators/distinctUntilChanged';
+import {merge} from 'rxjs/operators/merge';
+import {filter} from 'rxjs/operators/filter';
+import {Subject} from 'rxjs/Subject';
+import {NgbTypeahead, NgbTypeaheadSelectItemEvent} from '@ng-bootstrap/ng-bootstrap';
+import {StoreService} from '@eg/core/store.service';
+
+export interface TypeaheadEntry {
+  id: any;
+  label: string;
+  disabled?: boolean;
+}
+
+@Component({
+  selector: 'eg-typeahead',
+  templateUrl: './typeahead.component.html'
+})
+export class TypeaheadComponent implements OnInit {
+
+    selected: TypeaheadEntry;
+    click$: Subject<string>;
+    entrylist: TypeaheadEntry[];
+
+    @ViewChild('instance') instance: NgbTypeahead;
+
+    // Placeholder text for selector input
+    @Input() placeholder = '';
+
+    @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;
+
+    // Entry ID of the default entry to select (optional)
+    // onChange() is NOT fired when applying the default value
+    @Input() startId: any;
+
+    @Input() set entries(el: TypeaheadEntry[]) {
+        this.entrylist = el;
+    }
+
+    // Emitted when the org unit value is changed via the selector.
+    // Does not fire on initialOrg
+    @Output() onChange: EventEmitter<TypeaheadEntry>;
+
+    // Useful for massaging the match string prior to comparison
+    // Default version trims leading/trailing spaces
+    formatMatchString: (TypeaheadEntry) => string;
+
+    constructor(
+      private store: StoreService,
+    ) {
+        this.entrylist = [];
+        this.click$ = new Subject<string>();
+        this.onChange = new EventEmitter<TypeaheadEntry>();
+    }
+
+    ngOnInit() {
+
+        if (this.startId) {
+            this.selected = this.entrylist.filter(
+                e => e.id === this.startId)[0];
+        }
+
+        this.formatMatchString = (result: TypeaheadEntry) => {
+            return result.label.trim();
+        };
+    }
+
+    // Fired by the typeahead to inform us of a change.
+    // TODO: this does not fire when the value is cleared :( -- implement
+    // change detection on this.selected to look specifically for NULL.
+    selectorChanged(selEvent: NgbTypeaheadSelectItemEvent) {
+        this.onChange.emit(selEvent.item.id);
+    }
+
+    filter = (text$: Observable<string>): Observable<TypeaheadEntry[]> => {
+        return text$.pipe(
+            debounceTime(200),
+            distinctUntilChanged(),
+
+            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;
+                    }
+                }))
+            ),
+
+            map(term => {
+
+                if (term === '' || term === '_CLICK_') {
+                    // Click events display all visible entrylist
+                    return this.entrylist;
+                }
+
+                // Filter entrylist whose labels substring-match the
+                // text entered.
+                return this.entrylist.filter(entry => 
+                    entry.label.toLowerCase().indexOf(term.toLowerCase()) > -1
+                );
+            })
+        );
+    }
+}
+
+
index 2dfbb3c..a22b4e0 100644 (file)
@@ -1,6 +1,7 @@
 import {NgModule, ModuleWithProviders} from '@angular/core';
 import {EgCommonModule} from '@eg/common.module';
 import {StaffBannerComponent} from './share/staff-banner.component';
+import {TypeaheadComponent} from '@eg/share/typeahead/typeahead.component';
 import {OrgSelectComponent} from '@eg/share/org-select/org-select.component';
 import {AccessKeyDirective} from '@eg/share/accesskey/accesskey.directive';
 import {AccessKeyService} from '@eg/share/accesskey/accesskey.service';
@@ -20,6 +21,7 @@ import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
 @NgModule({
   declarations: [
     StaffBannerComponent,
+    TypeaheadComponent,
     OrgSelectComponent,
     AccessKeyDirective,
     AccessKeyInfoComponent,
@@ -35,6 +37,7 @@ import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
   exports: [
     EgCommonModule,
     StaffBannerComponent,
+    TypeaheadComponent,
     OrgSelectComponent,
     AccessKeyDirective,
     AccessKeyInfoComponent,
index 074389a..e560abc 100644 (file)
 
 <!-- Progress Dialog Experiments ----------------------------- -->
 <div class="row mb-3">
-  <eg-progress-dialog #progressDialog>
-  </eg-progress-dialog>
-  <button class="btn btn-light" (click)="showProgress()">Test Progress Dialog</button>
-</div>
-<!-- /Progress Dialog Experiments ----------------------------- -->
-
-<!-- eg toast -->
-<div class="row mb-3">
+  <div class="col-lg-4">
+    <eg-progress-dialog #progressDialog>
+    </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>
+  <div class="col-lg-4">
+    Typeahead: <eg-typeahead [entries]="taEntries" [clickShowsAll]="true"></eg-typeahead>
+  </div>
 </div>
+<!-- /Progress Dialog Experiments ----------------------------- -->
 
 <!-- eg strings -->
 <!--
index d0cee1b..d3f749a 100644 (file)
@@ -13,6 +13,7 @@ import {OrgService} from '@eg/core/org.service';
 import {Pager} from '@eg/share/util/pager';
 import {DateSelectComponent} from '@eg/share/date-select/date-select.component';
 import {PrintService} from '@eg/share/print/print.service';
+import {TypeaheadEntry} from '@eg/share/typeahead/typeahead.component';
 
 @Component({
   templateUrl: 'sandbox.component.html'
@@ -32,6 +33,8 @@ export class SandboxComponent implements OnInit {
 
     gridDataSource: GridDataSource = new GridDataSource();
 
+    taEntries: TypeaheadEntry[];
+
     btSource: GridDataSource = new GridDataSource();
     world = 'world'; // for local template version
     btGridTestContext: any = {hello : this.world};
@@ -54,7 +57,10 @@ export class SandboxComponent implements OnInit {
         private strings: StringService,
         private toast: ToastService,
         private printer: PrintService
-    ) {}
+    ) {
+    
+        this.taEntries = [];
+    }
 
     ngOnInit() {
 
@@ -64,6 +70,10 @@ export class SandboxComponent implements OnInit {
             {name: 'The Tick', state: 'TX'}
         ];
 
+        this.pcrud.retrieveAll('cmrcfld', {order_by:{cmrcfld: 'name'}})
+        .subscribe(format => 
+            this.taEntries.push({id: format.id(), label: format.name()}));
+
         this.btSource.getRows = (pager: Pager, sort: any[]) => {
 
             const orderBy: any = {cbt: 'name'};
@@ -81,6 +91,8 @@ export class SandboxComponent implements OnInit {
                 return cbt;
             }));
         };
+
+
     }
 
     btGridRowClassCallback(row: any): string {