From 2264d1c18ae9ceca3dc88cc8297cf3e6e1b1c312 Mon Sep 17 00:00:00 2001
From: Kai Glass <50838996+Tuxio@users.noreply.github.com>
Date: Sun, 2 Jul 2023 00:55:04 +0200
Subject: [PATCH] #2242 #12983 display selectedItem template in autocomplete
input
---
.../components/autocomplete/autocomplete.css | 21 +++-
.../autocomplete/autocomplete.spec.ts | 101 ++++++++++++++++-
.../components/autocomplete/autocomplete.ts | 103 ++++++++++++++----
3 files changed, 197 insertions(+), 28 deletions(-)
diff --git a/src/app/components/autocomplete/autocomplete.css b/src/app/components/autocomplete/autocomplete.css
index d30e4e7ce97..ac715e6217a 100755
--- a/src/app/components/autocomplete/autocomplete.css
+++ b/src/app/components/autocomplete/autocomplete.css
@@ -42,6 +42,10 @@
overflow: hidden;
}
+.p-autocomplete-template-item {
+ min-width: 145px;
+}
+
.p-autocomplete-multiple-container {
margin: 0;
padding: 0;
@@ -80,6 +84,21 @@
width: 100%;
}
+.p-autocomplete-template-input-token {
+ border: 0 none;
+ outline: 0 none;
+ background-color: transparent;
+ margin: 0;
+ margin-left: .5rem;
+ padding: 0;
+ box-shadow: none;
+ border-radius: 0;
+ width: 2rem;
+
+ flex: 1 1 auto;
+ display: inline-flex;
+}
+
.p-fluid .p-autocomplete {
display: flex;
}
@@ -97,4 +116,4 @@
.p-autocomplete-clearable {
position: relative;
-}
\ No newline at end of file
+}
diff --git a/src/app/components/autocomplete/autocomplete.spec.ts b/src/app/components/autocomplete/autocomplete.spec.ts
index 02f863bf2e3..cd13a809d29 100755
--- a/src/app/components/autocomplete/autocomplete.spec.ts
+++ b/src/app/components/autocomplete/autocomplete.spec.ts
@@ -11,9 +11,18 @@ import { ChevronDownIcon } from 'primeng/icons/chevrondown';
import { TimesCircleIcon } from 'primeng/icons/timescircle';
@Component({
- template: `
+ template: `
+
- `
+
+
+
+ Custom Text: {{ car.brand }}
+
+ `
})
class TestAutocompleteComponent {
brands: string[] = ['Audi', 'BMW', 'Fiat', 'Ford', 'Honda', 'Jaguar', 'Mercedes', 'Renault', 'Volvo', 'VW'];
@@ -22,6 +31,7 @@ class TestAutocompleteComponent {
filteredCars: any[];
brand: string;
car: any;
+ car2: any;
filterBrands(event) {
this.filteredBrands = [];
@@ -42,6 +52,12 @@ class TestAutocompleteComponent {
}
}
}
+
+ selectCustomCarOnComplete(event) {
+ this.filterBrandsWithField(event);
+ this.car2 = this.cars[0];
+ }
+
deleteLastEl() {
this.brands.pop();
}
@@ -50,6 +66,7 @@ class TestAutocompleteComponent {
describe('AutoComplete', () => {
let autocomplete: AutoComplete;
let autocomplete2: AutoComplete;
+ let autocomplete3: AutoComplete;
let testComponent: TestAutocompleteComponent;
let fixture: ComponentFixture;
@@ -62,6 +79,7 @@ describe('AutoComplete', () => {
fixture = TestBed.createComponent(TestAutocompleteComponent);
autocomplete = fixture.debugElement.children[0].componentInstance;
autocomplete2 = fixture.debugElement.children[2].componentInstance;
+ autocomplete3 = fixture.debugElement.children[3].componentInstance;
testComponent = fixture.debugElement.componentInstance;
});
@@ -454,6 +472,64 @@ describe('AutoComplete', () => {
flush();
}));
+ it('should display custom selectedItem template element', fakeAsync(() => {
+ autocomplete3.field = 'brand';
+ fixture.detectChanges();
+
+ autocomplete3.selectItem({ brand: 'Volvo' });
+ fixture.detectChanges();
+ expect(autocomplete3.value.brand).toEqual('Volvo');
+
+ const singleContainerElement = fixture.debugElement.queryAll(By.css('p-autoComplete'))[2].query(
+ By.css('.p-component.p-inputtext.p-autocomplete-template-item'));
+ const templateItemDiv = singleContainerElement.query(By.css('.p-autocomplete-token'));
+ const selectedItemSpanElement = templateItemDiv.query(By.css('span'));
+
+ expect(selectedItemSpanElement).toBeTruthy();
+ expect(selectedItemSpanElement.childNodes[0].nativeNode.data).toEqual("Custom Text: Volvo");
+ }));
+
+ it('should change value of custom selectedItem template on keyEnter', fakeAsync(() => {
+ autocomplete3.field = 'brand';
+ autocomplete3.forceSelection = true;
+ fixture.detectChanges();
+
+ autocomplete3.selectItem({ brand: 'Volvo' });
+ fixture.detectChanges();
+ expect(autocomplete3.value.brand).toEqual('Volvo');
+
+ const inputEl = fixture.debugElement.queryAll(By.css('p-autoComplete'))[2].query(
+ By.css('.p-autocomplete-template-input-token'));
+ inputEl.nativeElement.dispatchEvent(new Event('focus'));
+ inputEl.nativeElement.focus();
+ inputEl.nativeElement.click();
+ fixture.detectChanges();
+
+ inputEl.nativeElement.value = 'VW';
+ inputEl.nativeElement.dispatchEvent(new Event('keydown'));
+ inputEl.nativeElement.dispatchEvent(new Event('input'));
+ inputEl.nativeElement.dispatchEvent(new Event('keyup'));
+ tick(300);
+ fixture.detectChanges();
+
+ const firstItemEl = fixture.debugElement.queryAll(By.css('p-autoComplete'))[2].query(
+ By.css('li')).nativeElement;
+ firstItemEl.click();
+ fixture.detectChanges();
+ expect(autocomplete3.value.brand).toEqual('VW');
+ expect(inputEl.nativeElement.value).toEqual('');
+ expect(testComponent.car2).toEqual(autocomplete3.value);
+
+ const singleContainerElement = fixture.debugElement.queryAll(By.css('p-autoComplete'))[2].query(
+ By.css('.p-component.p-inputtext.p-autocomplete-template-item'));
+ const templateItemDiv = singleContainerElement.query(By.css('.p-autocomplete-token'));
+ const selectedItemSpanElement = templateItemDiv.query(By.css('span'));
+
+ expect(selectedItemSpanElement).toBeTruthy();
+ expect(selectedItemSpanElement.childNodes[0].nativeNode.data).toEqual("Custom Text: VW");
+ flush();
+ }));
+
it('should change minLength', fakeAsync(() => {
autocomplete.minLength = 2;
fixture.detectChanges();
@@ -470,7 +546,7 @@ describe('AutoComplete', () => {
tick(300);
fixture.detectChanges();
- const panelEl = fixture.debugElement.query(By.css('div'));
+ const panelEl = fixture.debugElement.query(By.css('div:not(.p-autocomplete-template-item)'));
expect(panelEl).toBeFalsy();
inputEl.nativeElement.value = 'va';
@@ -480,7 +556,7 @@ describe('AutoComplete', () => {
tick(300);
fixture.detectChanges();
- const updatedPanelEl = fixture.debugElement.query(By.css('div'));
+ const updatedPanelEl = fixture.debugElement.query(By.css('div:not(.p-autocomplete-template-item)'));
expect(updatedPanelEl).toBeFalsy();
flush();
}));
@@ -620,6 +696,23 @@ describe('AutoComplete', () => {
flush();
}));
+ it('should delete selectedItem template with backspace', fakeAsync(() => {
+ autocomplete3.field = 'brand';
+ fixture.detectChanges();
+
+ autocomplete3.selectItem({ brand: 'Volvo' });
+ expect(autocomplete3.value.brand).toEqual('Volvo');
+ fixture.detectChanges();
+
+ let backspaceEvent = new Event('keydown');
+ Object.defineProperty(backspaceEvent, 'which', { value: 8 });
+ Object.defineProperty(backspaceEvent, 'preventDefault', { value: () => {} });
+ autocomplete3.onKeydown(backspaceEvent);
+ fixture.detectChanges();
+
+ expect(autocomplete3.value).toEqual(null);
+ }));
+
it('should navigate with arrow keys and select with enter', () => {
fixture.detectChanges();
diff --git a/src/app/components/autocomplete/autocomplete.ts b/src/app/components/autocomplete/autocomplete.ts
index 5c094530215..862b3c8e61f 100755
--- a/src/app/components/autocomplete/autocomplete.ts
+++ b/src/app/components/autocomplete/autocomplete.ts
@@ -57,7 +57,7 @@ export const AUTOCOMPLETE_VALUE_ACCESSOR: any = {
+
-
@@ -592,6 +630,8 @@ export class AutoComplete implements AfterViewChecked, AfterContentInit, OnDestr
@ViewChild('multiIn') multiInputEl: Nullable;
+ @ViewChild('templateItemIn') templateItemInputEl: Nullable;
+
@ViewChild('multiContainer') multiContainerEL: Nullable;
@ViewChild('ddBtn') dropdownButton: Nullable;
@@ -885,15 +925,15 @@ export class AutoComplete implements AfterViewChecked, AfterContentInit, OnDestr
this.forceSelectionUpdateModelTimeout = null;
}
+ const value = (this.selectedItemTemplate == null && !this.multiple) ? this.resolveFieldData(option) : '';
+ this.inputElementRef.nativeElement.value = value;
if (this.multiple) {
- (this.multiInputEl as ElementRef).nativeElement.value = '';
this.value = this.value || [];
if (!this.isSelected(option) || !this.unique) {
this.value = [...this.value, option];
this.onModelChange(this.value);
}
} else {
- (this.inputEL as ElementRef).nativeElement.value = this.resolveFieldData(option);
this.value = option;
this.onModelChange(this.value);
}
@@ -910,8 +950,8 @@ export class AutoComplete implements AfterViewChecked, AfterContentInit, OnDestr
}
show(event?: Event) {
- if (this.multiInputEl || this.inputEL) {
- let hasFocus = this.multiple ? this.multiInputEl?.nativeElement.ownerDocument.activeElement == this.multiInputEl?.nativeElement : this.inputEL?.nativeElement.ownerDocument.activeElement == this.inputEL?.nativeElement;
+ if (this.multiInputEl || this.inputEL || this.templateItemInputEl) {
+ let hasFocus = this.inputElementRef?.nativeElement.ownerDocument.activeElement == this.inputElementRef?.nativeElement;
if (!this.overlayVisible && hasFocus) {
this.overlayVisible = true;
@@ -925,12 +965,11 @@ export class AutoComplete implements AfterViewChecked, AfterContentInit, OnDestr
clear() {
this.value = null;
this.inputValue = null;
- if (this.multiple) {
- (this.multiInputEl).nativeElement.value = '';
- } else {
+
+ if (!this.multiple) {
this.inputValue = null;
- (this.inputEL).nativeElement.value = '';
}
+ this.inputElementRef.nativeElement.value = '';
this.updateFilledState();
this.onModelChange(this.value);
@@ -959,7 +998,7 @@ export class AutoComplete implements AfterViewChecked, AfterContentInit, OnDestr
handleDropdownClick(event: Event) {
if (!this.overlayVisible) {
this.focusInput();
- let queryValue = this.multiple ? (this.multiInputEl as ElementRef).nativeElement.value : (this.inputEL as ElementRef).nativeElement.value;
+ let queryValue = this.inputElementRef.nativeElement.value;
if (this.dropdownMode === 'blank') this.search(event, '');
else if (this.dropdownMode === 'current') this.search(event, queryValue);
@@ -974,8 +1013,7 @@ export class AutoComplete implements AfterViewChecked, AfterContentInit, OnDestr
}
focusInput() {
- if (this.multiple) (this.multiInputEl as ElementRef).nativeElement.focus();
- else this.inputEL?.nativeElement.focus();
+ this.inputElementRef?.nativeElement.focus();
}
get emptyMessageLabel(): string {
@@ -1093,6 +1131,17 @@ export class AutoComplete implements AfterViewChecked, AfterContentInit, OnDestr
}
}
+ if (!this.multiple && this.selectedItemTemplate != null) {
+ switch ((event).which) {
+ //backspace
+ case 8:
+ if (this.value && !this.templateItemInputEl?.nativeElement.value) {
+ this.selectItem(null);
+ }
+ break;
+ }
+ }
+
if (this.multiple) {
switch ((event).which) {
//backspace
@@ -1117,7 +1166,7 @@ export class AutoComplete implements AfterViewChecked, AfterContentInit, OnDestr
onInputFocus(event: Event) {
if (!this.itemClicked && this.completeOnFocus) {
- let queryValue = this.multiple ? this.multiInputEl?.nativeElement.value : this.inputEL?.nativeElement.value;
+ let queryValue = this.inputElementRef?.nativeElement.value;
this.search(event, queryValue);
}
@@ -1158,13 +1207,7 @@ export class AutoComplete implements AfterViewChecked, AfterContentInit, OnDestr
}
if (!valid) {
- if (this.multiple) {
- (this.multiInputEl).nativeElement.value = '';
- } else {
- this.value = null;
- (this.inputEL).nativeElement.value = '';
- }
-
+ this.inputElementRef.nativeElement.value = '';
this.onClear.emit(event);
this.onModelChange(this.value);
this.updateFilledState();
@@ -1225,21 +1268,35 @@ export class AutoComplete implements AfterViewChecked, AfterContentInit, OnDestr
}
updateFilledState() {
- if (this.multiple) this.filled = (this.value && this.value.length) || (this.multiInputEl && this.multiInputEl.nativeElement && this.multiInputEl.nativeElement.value != '');
- else this.filled = (this.inputFieldValue && this.inputFieldValue != '') || (this.inputEL && this.inputEL.nativeElement && this.inputEL.nativeElement.value != '');
+ this.filled =
+ (this.multiple ? (this.value && this.value.length) : (this.inputFieldValue && this.inputFieldValue != ''))
+ || (this.inputElementRef?.nativeElement.value != '');
}
updateInputField() {
let formattedValue = this.resolveFieldData(this.value);
this.inputFieldValue = formattedValue;
- if (this.inputEL && this.inputEL.nativeElement) {
+ if (this.inputEL && this.inputEL.nativeElement && this.selectedItemTemplate == null) {
this.inputEL.nativeElement.value = formattedValue;
}
this.updateFilledState();
}
+ /**
+ * Returns ElementRef based on used input in HTML template
+ */
+ get inputElementRef(): ElementRef {
+ if (this.multiple) {
+ return this.multiInputEl;
+ } else if (this.selectedItemTemplate != null) {
+ return this.templateItemInputEl;
+ } else {
+ return this.inputEL;
+ }
+ }
+
ngOnDestroy() {
if (this.forceSelectionUpdateModelTimeout) {
clearTimeout(this.forceSelectionUpdateModelTimeout);