LP1842763 Combobox readonly, domId, and more
authorBill Erickson <berickxx@gmail.com>
Wed, 12 May 2021 16:00:07 +0000 (12:00 -0400)
committerBill Erickson <berickxx@gmail.com>
Wed, 12 May 2021 16:15:15 +0000 (12:15 -0400)
* Readonly support which just displays the selected value.
* domId in put support
* smallFormContron option
* append to the IDL query with a new idlQueryAnd input
* Support for applying a freetext startId value
* Fixes issues with click-to-show-all support

Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/share/combobox/combobox.component.html
Open-ILS/src/eg2/src/app/share/combobox/combobox.component.ts

index d228081..6d2131e 100644 (file)
   {{r.fm.name()}} ({{getOrgShortname(r.fm.owning_lib())}})
 </ng-template>
 
-<div class="d-flex">
-  <input type="text" 
-    class="form-control"
-    [ngClass]="{'text-success font-italic font-weight-bold': selected && selected.freetext}"
-    [placeholder]="placeholder"
-    [name]="name"
-    [disabled]="isDisabled"
-    [required]="isRequired"
-    [(ngModel)]="selected" 
-    [ngbTypeahead]="filter"
-    [resultTemplate]="getResultTemplate()"
-    [inputFormatter]="formatDisplayString"
-    (click)="onClick($event)"
-    (blur)="onBlur()"
-    container="body"
-    (selectItem)="selectorChanged($event)"
-    #instance="ngbTypeahead"/>
-  <div class="d-flex flex-column icons" (click)="openMe($event)">
-    <span class="material-icons">keyboard_arrow_up</span>
-    <span class="material-icons">keyboard_arrow_down</span>
+<ng-container *ngIf="readOnly">
+  <span *ngIf="!selected" i18n>&lt;Unset&gt;</span>
+  <ng-container *ngIf="selected">
+    <ng-container *ngTemplateOutlet="getResultTemplate();context:{result: selected}">
+  </ng-container>
+</ng-container>
+
+<ng-container *ngIf="!readOnly">
+  <div class="d-flex">
+    <input type="text" 
+      class="form-control"
+      [id]="domId"
+      [ngClass]="{
+        'text-success font-italic font-weight-bold': selected && selected.freetext,
+        'form-control-sm': smallFormControl
+      }"
+      [placeholder]="placeholder"
+      [name]="name"
+      [disabled]="isDisabled"
+      [required]="isRequired"
+      [(ngModel)]="selected" 
+      [ngbTypeahead]="filter"
+      [resultTemplate]="getResultTemplate()"
+      [inputFormatter]="formatDisplayString"
+      (click)="onClick($event)"
+      (blur)="onBlur()"
+      container="body"
+      (selectItem)="selectorChanged($event)"
+      #instance="ngbTypeahead"/>
+    <div class="d-flex flex-column icons" (click)="openMe($event)">
+      <span class="material-icons">keyboard_arrow_up</span>
+      <span class="material-icons">keyboard_arrow_down</span>
+    </div>
   </div>
-</div>
+</ng-container>
index d21ad5f..7d03b2f 100644 (file)
@@ -46,16 +46,21 @@ export class IdlClassTemplateDirective {
     multi: true
   }]
 })
-export class ComboboxComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges {
+export class ComboboxComponent
+    implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges {
+
+    static domIdAuto = 0;
 
     selected: ComboboxEntry;
     click$: Subject<string>;
     entrylist: ComboboxEntry[];
 
-    @ViewChild('instance', { static: true }) instance: NgbTypeahead;
-    @ViewChild('defaultDisplayTemplate', { static: true}) defaultDisplayTemplate: TemplateRef<any>;
+    @ViewChild('instance', {static: false}) instance: NgbTypeahead;
+    @ViewChild('defaultDisplayTemplate', {static: true}) defaultDisplayTemplate: TemplateRef<any>;
     @ViewChildren(IdlClassTemplateDirective) idlClassTemplates: QueryList<IdlClassTemplateDirective>;
 
+    @Input() domId = 'eg-combobox-' + ComboboxComponent.domIdAuto++;
+
     // Applies a name attribute to the input.
     // Useful in forms.
     @Input() name: string;
@@ -69,6 +74,9 @@ export class ComboboxComponent implements ControlValueAccessor, OnInit, AfterVie
 
     @Input() inputSize: number = null;
 
+    // If true, applies form-control-sm CSS
+    @Input() smallFormControl = false;
+
     // Add a 'required' attribute to the input
     isRequired: boolean;
     @Input() set required(r: boolean) {
@@ -88,13 +96,24 @@ export class ComboboxComponent implements ControlValueAccessor, OnInit, AfterVie
     @Input() idlClass: string;
     @Input() startIdFiresOnChange: boolean;
 
+    // This will be appended to the async data retrieval query
+    // when fetching objects by idlClass.
+    @Input() idlQueryAnd: {[field: string]: any};
+
+    // Display the selected value as text instead of within
+    // the typeahead
+    @Input() readOnly = false;
+
     // Allow the selected entry ID to be passed via the template
     // This does NOT not emit onChange events.
     @Input() set selectedId(id: any) {
         if (id === undefined) { return; }
 
         // clear on explicit null
-        if (id === null) { this.selected = null; }
+        if (id === null) {
+            this.selected = null;
+            return;
+        }
 
         if (this.entrylist.length) {
             this.selected = this.entrylist.filter(e => e.id === id)[0];
@@ -239,6 +258,9 @@ export class ComboboxComponent implements ControlValueAccessor, OnInit, AfterVie
                 const args = {};
                 const extra_args = { order_by : {} };
                 args[field] = {'ilike': `%${term}%`}; // could -or search on label
+                if (this.idlQueryAnd) {
+                    Object.assign(args, this.idlQueryAnd);
+                }
                 extra_args['order_by'][this.idlClass] = field;
                 extra_args['limit'] = 100;
                 if (this.idlIncludeLibraryInLabel) {
@@ -358,8 +380,7 @@ export class ComboboxComponent implements ControlValueAccessor, OnInit, AfterVie
     // Apply a default selection where needed
     applySelection() {
 
-        if (this.startId !== null &&
-            this.entrylist && !this.defaultSelectionApplied) {
+        if (this.entrylist && !this.defaultSelectionApplied) {
 
             const entry =
                 this.entrylist.filter(e => e.id === this.startId)[0];
@@ -389,6 +410,7 @@ export class ComboboxComponent implements ControlValueAccessor, OnInit, AfterVie
     }
 
     addAsyncEntry(entry: ComboboxEntry) {
+        if (!entry) { return; }
         // Avoid duplicate async entries
         if (!this.asyncIds['' + entry.id]) {
             this.asyncIds['' + entry.id] = true;
@@ -407,13 +429,25 @@ export class ComboboxComponent implements ControlValueAccessor, OnInit, AfterVie
         if (typeof this.selected === 'string') {
 
             if (this.allowFreeText && this.selected !== '') {
-                // Free text entered which does not match a known entry
-                // translate it into a dummy ComboboxEntry
-                this.selected = {
-                    id: null,
-                    label: this.selected,
-                    freetext: true
-                };
+                const freeText = this.entrylist.filter(e => e.id === null)[0];
+
+                if (freeText) {
+
+                    // If we already had a free text entry, just replace
+                    // the label with the new value
+                    freeText.label = this.selected;
+                    this.selected = freeText;
+
+                }  else {
+
+                    // Free text entered which does not match a known entry
+                    // translate it into a dummy ComboboxEntry
+                    this.selected = {
+                        id: null,
+                        label: this.selected,
+                        freetext: true
+                    };
+                }
 
             } else {
 
@@ -442,12 +476,14 @@ export class ComboboxComponent implements ControlValueAccessor, OnInit, AfterVie
             return of(term);
         }
 
-        let searchTerm: string;
-        searchTerm = term;
-        if (searchTerm === '_CLICK_') {
+        let searchTerm = term;
+        if (term === '_CLICK_') {
             if (this.asyncSupportsEmptyTermClick) {
+                // Search for "all", but retain and propage the _CLICK_
+                // term so the filter knows to open the selector
                 searchTerm = '';
             } else {
+                // Skip the final filter map and display nothing.
                 return of();
             }
         }
@@ -457,7 +493,7 @@ export class ComboboxComponent implements ControlValueAccessor, OnInit, AfterVie
                 (entry: ComboboxEntry) => this.addAsyncEntry(entry),
                 err => {},
                 ()  => {
-                    observer.next(searchTerm);
+                    observer.next(term);
                     observer.complete();
                 }
             );
@@ -487,10 +523,8 @@ export class ComboboxComponent implements ControlValueAccessor, OnInit, AfterVie
                 // click action occurred.
                 if (term === '') { return []; }
 
-                // In sync-data mode, a click displays the full list.
-                if (term === '_CLICK_' && !this.asyncDataSource) {
-                    return this.entrylist;
-                }
+                // If we make it this far, _CLICK_ means show everything.
+                if (term === '_CLICK_') { term = ''; }
 
                 // Filter entrylist whose labels substring-match the
                 // text entered.