diff --git a/e2e/issues/issue-749.spec.ts b/e2e/issues/issue-749.spec.ts
new file mode 100644
index 0000000000..ace7e5023b
--- /dev/null
+++ b/e2e/issues/issue-749.spec.ts
@@ -0,0 +1,142 @@
+import { test, expect } from '@playwright/test';
+
+test.describe('Issue #749: Typeahead object value display fix', () => {
+ test.beforeEach(async ({ page }) => {
+ await page.goto('/');
+ await page.click('text=Typeahead');
+ });
+
+ test('should display object values correctly in typeahead input', async ({ page }) => {
+ // Wait for typeahead demo to load
+ await page.waitForSelector('[placeholder="Locations loaded from API"]');
+
+ // Look for a typeahead that might have object values
+ const input = page.locator('[placeholder="Locations loaded from API"]').first();
+
+ // Check that input doesn't show [object Object]
+ const inputValue = await input.inputValue();
+ expect(inputValue).not.toBe('[object Object]');
+
+ // If there's an initial value, it should be readable
+ if (inputValue && inputValue.length > 0) {
+ expect(inputValue).toMatch(/^[a-zA-Z0-9\s]+$/); // Should be readable text
+ }
+ });
+
+ test('should handle object selection and display correctly', async ({ page }) => {
+ await page.waitForSelector('[placeholder="Locations loaded from API"]');
+
+ const input = page.locator('[placeholder="Locations loaded from API"]').first();
+ await input.click();
+ await input.fill('Cal');
+
+ // Wait for dropdown to appear
+ await page.waitForSelector('.dropdown-menu', { state: 'visible' });
+
+ // Select first item
+ const firstItem = page.locator('.dropdown-item').first();
+ await firstItem.click();
+
+ // Input should show proper text, not [object Object]
+ const selectedValue = await input.inputValue();
+ expect(selectedValue).toBeTruthy();
+ expect(selectedValue).not.toBe('[object Object]');
+ expect(selectedValue.length).toBeGreaterThan(0);
+ });
+
+ test('should maintain functionality after object value fix', async ({ page }) => {
+ // Ensure the fix doesn't break existing typeahead functionality
+ await page.waitForSelector('[placeholder="Locations loaded from API"]');
+
+ const input = page.locator('[placeholder="Locations loaded from API"]').first();
+
+ // Test typing and filtering
+ await input.click();
+ await input.fill('Ala');
+
+ // Should show filtered results
+ await page.waitForSelector('.dropdown-menu', { state: 'visible' });
+
+ const items = page.locator('.dropdown-item');
+ const itemCount = await items.count();
+ expect(itemCount).toBeGreaterThan(0);
+
+ // Items should contain the typed text
+ const firstItemText = await items.first().textContent();
+ expect(firstItemText).toContain('Ala');
+ });
+
+ test('should handle keyboard navigation with object values', async ({ page }) => {
+ await page.waitForSelector('[placeholder="Locations loaded from API"]');
+
+ const input = page.locator('[placeholder="Locations loaded from API"]').first();
+ await input.click();
+ await input.fill('A');
+
+ // Wait for dropdown
+ await page.waitForSelector('.dropdown-menu', { state: 'visible' });
+
+ // Navigate with arrow keys
+ await page.keyboard.press('ArrowDown');
+ await page.keyboard.press('ArrowDown');
+ await page.keyboard.press('Enter');
+
+ // Should select properly without showing [object Object]
+ const selectedValue = await input.inputValue();
+ expect(selectedValue).toBeTruthy();
+ expect(selectedValue).not.toBe('[object Object]');
+ expect(selectedValue).not.toBe('[object HTMLElement]');
+ });
+
+ test('should handle edge cases for object value display', async ({ page }) => {
+ // Test various scenarios that might cause [object Object] display
+ await page.waitForSelector('[placeholder="Locations loaded from API"]');
+
+ const input = page.locator('[placeholder="Locations loaded from API"]').first();
+
+ // Test clearing and re-entering values
+ await input.click();
+ await input.fill('');
+ await input.fill('Test');
+
+ // Clear again
+ await input.fill('');
+
+ // Should not show [object Object] in any state
+ const inputValue = await input.inputValue();
+ expect(inputValue).not.toBe('[object Object]');
+ });
+
+ test('should work with different typeahead configurations', async ({ page }) => {
+ // Test that the fix works across different typeahead setups
+ await page.waitForSelector('input[typeahead]').catch(() => {
+ // Fallback if specific selector doesn't exist
+ return page.waitForSelector('input');
+ });
+
+ // Find any typeahead inputs on the page
+ const typeaheadInputs = page.locator('input[typeahead], input[placeholder*="type"], input[placeholder*="search"]');
+ const count = await typeaheadInputs.count();
+
+ if (count > 0) {
+ for (let i = 0; i < Math.min(count, 3); i++) {
+ const input = typeaheadInputs.nth(i);
+ const inputValue = await input.inputValue();
+
+ // None should show [object Object]
+ expect(inputValue).not.toBe('[object Object]');
+
+ // If visible and enabled, test basic functionality
+ if (await input.isVisible() && await input.isEnabled()) {
+ await input.click();
+ await input.fill('test');
+ await input.fill('');
+
+ // Still shouldn't show [object Object]
+ const clearedValue = await input.inputValue();
+ expect(clearedValue).not.toBe('[object Object]');
+ }
+ }
+ }
+ });
+});
\ No newline at end of file
diff --git a/src/typeahead/testing/typeahead-object-value.spec.ts b/src/typeahead/testing/typeahead-object-value.spec.ts
new file mode 100644
index 0000000000..77641318cd
--- /dev/null
+++ b/src/typeahead/testing/typeahead-object-value.spec.ts
@@ -0,0 +1,180 @@
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { Component, ViewChild } from '@angular/core';
+import { FormsModule } from '@angular/forms';
+import { TypeaheadDirective } from '../typeahead.directive';
+import { TypeaheadModule } from '../typeahead.module';
+
+@Component({
+ template: `
+
+ `
+})
+class TestTypeaheadObjectValueComponent {
+ @ViewChild(TypeaheadDirective, { static: true }) typeahead!: TypeaheadDirective;
+
+ selectedState = { name: 'California', code: 'CA' }; // Initial object value
+
+ states = [
+ { name: 'Alabama', code: 'AL' },
+ { name: 'Alaska', code: 'AK' },
+ { name: 'Arizona', code: 'AZ' },
+ { name: 'Arkansas', code: 'AR' },
+ { name: 'California', code: 'CA' }
+ ];
+}
+
+@Component({
+ template: `
+
+ `
+})
+class TestTypeaheadNoOptionFieldComponent {
+ @ViewChild(TypeaheadDirective, { static: true }) typeahead!: TypeaheadDirective;
+
+ selectedState = { name: 'California', code: 'CA' }; // Initial object value with no typeaheadOptionField
+
+ states = [
+ { name: 'Alabama', code: 'AL' },
+ { name: 'Alaska', code: 'AK' },
+ { name: 'California', code: 'CA' }
+ ];
+}
+
+describe('TypeaheadDirective - Object Value Display Issue #749', () => {
+ let component: TestTypeaheadObjectValueComponent;
+ let fixture: ComponentFixture;
+ let noFieldComponent: TestTypeaheadNoOptionFieldComponent;
+ let noFieldFixture: ComponentFixture;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [TestTypeaheadObjectValueComponent, TestTypeaheadNoOptionFieldComponent],
+ imports: [FormsModule, TypeaheadModule.forRoot()]
+ }).compileComponents();
+ });
+
+ describe('With typeaheadOptionField specified', () => {
+ beforeEach(() => {
+ fixture = TestBed.createComponent(TestTypeaheadObjectValueComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should display object property value when typeaheadOptionField is set', () => {
+ const input = fixture.nativeElement.querySelector('input');
+
+ // Input should display the name property, not "[object Object]"
+ expect(input.value).toBe('California');
+ expect(input.value).not.toBe('[object Object]');
+ });
+
+ it('should maintain object value in ngModel', () => {
+ expect(component.selectedState).toEqual({ name: 'California', code: 'CA' });
+ expect(typeof component.selectedState).toBe('object');
+ });
+
+ it('should handle selection and update display correctly', () => {
+ const input = fixture.nativeElement.querySelector('input');
+
+ // Clear and type new value
+ input.value = 'Alab';
+ input.dispatchEvent(new Event('input'));
+ fixture.detectChanges();
+
+ // Should be able to select from dropdown
+ expect(component.typeahead).toBeTruthy();
+ });
+ });
+
+ describe('Without typeaheadOptionField (auto-detect common fields)', () => {
+ beforeEach(() => {
+ noFieldFixture = TestBed.createComponent(TestTypeaheadNoOptionFieldComponent);
+ noFieldComponent = noFieldFixture.componentInstance;
+ noFieldFixture.detectChanges();
+ });
+
+ it('should auto-detect name field and display correctly', () => {
+ const input = noFieldFixture.nativeElement.querySelector('input');
+
+ // After fix, should detect 'name' field automatically
+ expect(input.value).toBe('California');
+ expect(input.value).not.toBe('[object Object]');
+ });
+
+ it('should have object value in ngModel', () => {
+ expect(noFieldComponent.selectedState).toEqual({ name: 'California', code: 'CA' });
+ expect(typeof noFieldComponent.selectedState).toBe('object');
+ });
+ });
+
+ describe('Object value handling utilities', () => {
+ it('should handle getValueFromObject correctly', () => {
+ const { getValueFromObject } = require('../typeahead-utils');
+
+ const testObject = { name: 'California', code: 'CA' };
+
+ // With option field specified
+ expect(getValueFromObject(testObject, 'name')).toBe('California');
+ expect(getValueFromObject(testObject, 'code')).toBe('CA');
+
+ // Without option field (auto-detect 'name')
+ expect(getValueFromObject(testObject, undefined)).toBe('California');
+ });
+
+ it('should auto-detect common display fields', () => {
+ const { getValueFromObject } = require('../typeahead-utils');
+
+ // Test different common field names
+ expect(getValueFromObject({ name: 'Test' }, undefined)).toBe('Test');
+ expect(getValueFromObject({ label: 'Test Label' }, undefined)).toBe('Test Label');
+ expect(getValueFromObject({ title: 'Test Title' }, undefined)).toBe('Test Title');
+ expect(getValueFromObject({ text: 'Test Text' }, undefined)).toBe('Test Text');
+ expect(getValueFromObject({ value: 'Test Value' }, undefined)).toBe('Test Value');
+ expect(getValueFromObject({ display: 'Test Display' }, undefined)).toBe('Test Display');
+
+ // Should prioritize 'name' over other fields
+ expect(getValueFromObject({ name: 'Name', label: 'Label' }, undefined)).toBe('Name');
+ });
+
+ it('should fallback to [object Object] for objects without common fields', () => {
+ const { getValueFromObject } = require('../typeahead-utils');
+
+ const objectWithoutCommonFields = { id: 1, customField: 'value' };
+ expect(getValueFromObject(objectWithoutCommonFields, undefined)).toBe('[object Object]');
+ });
+
+ it('should handle nested properties', () => {
+ const { getValueFromObject } = require('../typeahead-utils');
+
+ const nestedObject = {
+ location: {
+ state: { name: 'California' },
+ country: 'USA'
+ }
+ };
+
+ expect(getValueFromObject(nestedObject, 'location.state.name')).toBe('California');
+ expect(getValueFromObject(nestedObject, 'location.country')).toBe('USA');
+ });
+
+ it('should handle method calls', () => {
+ const { getValueFromObject } = require('../typeahead-utils');
+
+ const objectWithMethod = {
+ name: 'California',
+ getName: function() { return this.name; }
+ };
+
+ expect(getValueFromObject(objectWithMethod, 'getName()')).toBe('California');
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/typeahead/typeahead-utils.ts b/src/typeahead/typeahead-utils.ts
index 851f9243a7..ae7292dddb 100644
--- a/src/typeahead/typeahead-utils.ts
+++ b/src/typeahead/typeahead-utils.ts
@@ -61,7 +61,19 @@ function tokenizeWordsAndPhrases(str: string, wordRegexDelimiters: string, phras
// eslint-disable-next-line
export function getValueFromObject(object: string | Record, option?: string): string {
- if (!option || typeof object !== 'object') {
+ if (typeof object !== 'object') {
+ return object.toString();
+ }
+
+ // If no option specified, try common display field names
+ if (!option) {
+ const commonFields = ['name', 'label', 'title', 'text', 'value', 'display'];
+ for (const field of commonFields) {
+ if (field in object && object[field] != null) {
+ return object[field].toString();
+ }
+ }
+ // Fallback to object.toString() which will show [object Object]
return object.toString();
}
diff --git a/src/typeahead/typeahead.directive.ts b/src/typeahead/typeahead.directive.ts
index 4a074a05c2..f13c56efba 100644
--- a/src/typeahead/typeahead.directive.ts
+++ b/src/typeahead/typeahead.directive.ts
@@ -236,6 +236,9 @@ export class TypeaheadDirective implements OnInit, OnDestroy {
}
this.checkDelimitersConflict();
+
+ // Handle initial object value display
+ setTimeout(() => this.handleInitialObjectValue());
}
@HostListener('input', ['$event'])
@@ -446,6 +449,20 @@ export class TypeaheadDirective implements OnInit, OnDestroy {
this._typeahead.dispose();
}
+ protected handleInitialObjectValue(): void {
+ const currentValue = this.ngControl.control?.value;
+
+ // Check if current value is an object and we have an option field specified
+ if (currentValue && typeof currentValue === 'object' && this.typeaheadOptionField) {
+ const displayValue = getValueFromObject(currentValue, this.typeaheadOptionField);
+
+ // Update the input element's display value if it's different
+ if (this.element.nativeElement.value !== displayValue) {
+ this.renderer.setProperty(this.element.nativeElement, 'value', displayValue);
+ }
+ }
+ }
+
protected asyncActions(): void {
this._subscriptions.push(
this.keyUpEventEmitter