diff --git a/dev/playground/combo-box.html b/dev/playground/combo-box.html
index dc5def7a54d..15945f1f4b4 100644
--- a/dev/playground/combo-box.html
+++ b/dev/playground/combo-box.html
@@ -59,24 +59,47 @@
States
+
+ Item Label Generator
+
+
+
diff --git a/packages/combo-box/src/vaadin-combo-box-items-mixin.d.ts b/packages/combo-box/src/vaadin-combo-box-items-mixin.d.ts
index 286e130590c..6be65eb0ef6 100644
--- a/packages/combo-box/src/vaadin-combo-box-items-mixin.d.ts
+++ b/packages/combo-box/src/vaadin-combo-box-items-mixin.d.ts
@@ -29,6 +29,13 @@ export declare class ComboBoxItemsMixinClass {
*/
filter: string;
+ /**
+ * A function that is used to generate the label for dropdown
+ * items based on the item. Receives one argument:
+ * - `item` The item to generate the label for.
+ */
+ itemLabelGenerator: ((item: TItem) => string) | undefined;
+
/**
* Path for label of the item. If `items` is an array of objects, the
* `itemLabelPath` is used to fetch the displayed string label for each
diff --git a/packages/combo-box/src/vaadin-combo-box-items-mixin.js b/packages/combo-box/src/vaadin-combo-box-items-mixin.js
index 2bbc39e2f55..825b3d890d4 100644
--- a/packages/combo-box/src/vaadin-combo-box-items-mixin.js
+++ b/packages/combo-box/src/vaadin-combo-box-items-mixin.js
@@ -77,6 +77,15 @@ export const ComboBoxItemsMixin = (superClass) =>
sync: true,
},
+ /**
+ * A function that is used to generate the label for dropdown
+ * items based on the item. Receives one argument:
+ * - `item` The item to generate the label for.
+ */
+ itemLabelGenerator: {
+ type: Object,
+ },
+
/**
* Path for label of the item. If `items` is an array of objects, the
* `itemLabelPath` is used to fetch the displayed string label for each
@@ -122,6 +131,10 @@ export const ComboBoxItemsMixin = (superClass) =>
if (props.has('filter')) {
this._filterChanged(this.filter);
}
+
+ if (props.has('itemLabelGenerator')) {
+ this.requestContentUpdate();
+ }
}
/**
@@ -161,6 +174,10 @@ export const ComboBoxItemsMixin = (superClass) =>
* @override
*/
_getItemLabel(item) {
+ if (typeof this.itemLabelGenerator === 'function' && item) {
+ return this.itemLabelGenerator(item) || '';
+ }
+
let label = item && this.itemLabelPath ? get(this.itemLabelPath, item) : undefined;
if (label === undefined || label === null) {
label = item ? item.toString() : '';
diff --git a/packages/combo-box/test/items.test.js b/packages/combo-box/test/items.test.js
index dadc0fb5334..c4984f9f2aa 100644
--- a/packages/combo-box/test/items.test.js
+++ b/packages/combo-box/test/items.test.js
@@ -142,8 +142,6 @@ describe('items', () => {
});
describe('itemClassNameGenerator', () => {
- let comboBox;
-
beforeEach(async () => {
comboBox = fixtureSync('');
await nextRender();
@@ -194,4 +192,76 @@ describe('items', () => {
expect(items[2].className).to.equal('');
});
});
+
+ describe('itemLabelGenerator', () => {
+ beforeEach(async () => {
+ comboBox = fixtureSync('');
+ comboBox.items = [
+ { id: 1, name: 'John', surname: 'Doe', age: 30 },
+ { id: 2, name: 'Jane', surname: 'Smith', age: 25 },
+ { id: 3, name: 'Bob', surname: 'Johnson', age: 35 },
+ ];
+ comboBox.itemLabelGenerator = (item) => `${item.name} ${item.surname}`;
+ await nextRender();
+ });
+
+ it('should generate items text content using itemLabelGenerator', async () => {
+ comboBox.open();
+ await nextRender();
+
+ const items = getAllItems(comboBox);
+ expect(items[0].textContent).to.equal('John Doe');
+ expect(items[1].textContent).to.equal('Jane Smith');
+ expect(items[2].textContent).to.equal('Bob Johnson');
+ });
+
+ it('should set generated label as input value when item is selected', async () => {
+ comboBox.itemValuePath = 'id';
+ comboBox.value = 2;
+ await nextRender();
+ expect(comboBox.inputElement.value).to.equal('Jane Smith');
+ });
+
+ it('should filter items using generated label', () => {
+ setInputValue(comboBox, 'john');
+
+ expect(comboBox.filteredItems.length).to.equal(2);
+ expect(comboBox.filteredItems[0]).to.deep.equal(comboBox.items[0]);
+ expect(comboBox.filteredItems[1]).to.deep.equal(comboBox.items[2]);
+ });
+
+ it('should use itemLabelGenerator over itemLabelPath', async () => {
+ comboBox.itemLabelPath = 'surname';
+ comboBox.itemLabelGenerator = (item) => item.name;
+ comboBox.open();
+ await nextRender();
+
+ const items = getAllItems(comboBox);
+ expect(items[0].textContent).to.equal('John');
+ expect(items[1].textContent).to.equal('Jane');
+ });
+
+ it('should accept empty string returned from itemLabelGenerator', async () => {
+ comboBox.itemLabelGenerator = (item) => (item.id === 2 ? '' : `${item.name} ${item.surname}`);
+ comboBox.open();
+ await nextRender();
+
+ const items = getAllItems(comboBox);
+ expect(items[0].textContent).to.equal('John Doe');
+ expect(items[1].textContent).to.equal('');
+ expect(items[2].textContent).to.equal('Bob Johnson');
+ });
+
+ it('should update dropdown when itemLabelGenerator changes', async () => {
+ comboBox.open();
+ await nextRender();
+
+ expect(getFirstItem(comboBox).textContent).to.equal('John Doe');
+
+ comboBox.itemLabelGenerator = (item) => `${item.name} (${item.age})`;
+ await nextRender();
+
+ expect(getFirstItem(comboBox).textContent).to.equal('John (30)');
+ });
+ });
});
diff --git a/packages/combo-box/test/typings/combo-box.types.ts b/packages/combo-box/test/typings/combo-box.types.ts
index 50caf56517f..eda20a33df8 100644
--- a/packages/combo-box/test/typings/combo-box.types.ts
+++ b/packages/combo-box/test/typings/combo-box.types.ts
@@ -100,6 +100,7 @@ assertType(narrowedComboBox.filter);
assertType(narrowedComboBox.filteredItems);
assertType(narrowedComboBox.items);
assertType<(item: TestComboBoxItem) => string>(narrowedComboBox.itemClassNameGenerator);
+assertType<((item: TestComboBoxItem) => string) | undefined>(narrowedComboBox.itemLabelGenerator);
assertType(narrowedComboBox.itemIdPath);
assertType(narrowedComboBox.itemLabelPath);
assertType(narrowedComboBox.itemValuePath);
diff --git a/packages/multi-select-combo-box/test/chips.test.js b/packages/multi-select-combo-box/test/chips.test.js
index 513d98d8ec5..1cc0021f055 100644
--- a/packages/multi-select-combo-box/test/chips.test.js
+++ b/packages/multi-select-combo-box/test/chips.test.js
@@ -2,12 +2,11 @@ import { expect } from '@vaadin/chai-plugins';
import { sendKeys } from '@vaadin/test-runner-commands';
import { fixtureSync, nextRender, nextResize, nextUpdate } from '@vaadin/testing-helpers';
import '../src/vaadin-multi-select-combo-box.js';
+import { getChips } from './helpers.js';
describe('chips', () => {
let comboBox, inputElement;
- const getChips = (combo) => combo.querySelectorAll('vaadin-multi-select-combo-box-chip');
-
const getChipContent = (chip) => chip.shadowRoot.querySelector('[part="label"]').textContent;
beforeEach(async () => {
diff --git a/packages/multi-select-combo-box/test/helpers.js b/packages/multi-select-combo-box/test/helpers.js
index fb820862d8c..f8339a123a9 100644
--- a/packages/multi-select-combo-box/test/helpers.js
+++ b/packages/multi-select-combo-box/test/helpers.js
@@ -1,3 +1,5 @@
+import { fire } from '@vaadin/testing-helpers';
+
export const getDataProvider = (allItems) => (params, callback) => {
const offset = params.page * params.pageSize;
const filteredItems = allItems.filter((item) => item.indexOf(params.filter) > -1);
@@ -31,3 +33,19 @@ export const getAllItems = (comboBox) => {
export const getFirstItem = (comboBox) => {
return comboBox._scroller.querySelector('vaadin-multi-select-combo-box-item');
};
+
+/**
+ * Emulates the user filling in something in the combo-box input.
+ *
+ * @param {Element} comboBox
+ * @param {string} value
+ */
+export function setInputValue(comboBox, value) {
+ comboBox.inputElement.value = value;
+ fire(comboBox.inputElement, 'input');
+}
+
+/**
+ * Returns all the chips of the combo-box.
+ */
+export const getChips = (comboBox) => comboBox.querySelectorAll('vaadin-multi-select-combo-box-chip');
diff --git a/packages/multi-select-combo-box/test/items.test.js b/packages/multi-select-combo-box/test/items.test.js
index 9e2cfc327f7..7f69e1dae8b 100644
--- a/packages/multi-select-combo-box/test/items.test.js
+++ b/packages/multi-select-combo-box/test/items.test.js
@@ -2,7 +2,7 @@ import { expect } from '@vaadin/chai-plugins';
import { fixtureSync, nextRender } from '@vaadin/testing-helpers';
import sinon from 'sinon';
import '../src/vaadin-multi-select-combo-box.js';
-import { getAllItems, getFirstItem } from './helpers.js';
+import { getAllItems, getChips, getFirstItem, setInputValue } from './helpers.js';
describe('items', () => {
let comboBox;
@@ -81,10 +81,6 @@ describe('items', () => {
});
describe('itemClassNameGenerator', () => {
- let comboBox;
-
- const getChips = (combo) => combo.querySelectorAll('vaadin-multi-select-combo-box-chip');
-
beforeEach(async () => {
comboBox = fixtureSync('');
await nextRender();
@@ -184,4 +180,95 @@ describe('items', () => {
expect(chips[2].className).to.equal('');
});
});
+
+ describe('itemLabelGenerator', () => {
+ beforeEach(async () => {
+ comboBox = fixtureSync(`
+
+ `);
+ comboBox.items = [
+ { id: 1, name: 'John', surname: 'Doe', age: 30 },
+ { id: 2, name: 'Jane', surname: 'Smith', age: 25 },
+ { id: 3, name: 'Bob', surname: 'Johnson', age: 35 },
+ ];
+ comboBox.itemLabelGenerator = (item) => `${item.name} ${item.surname}`;
+ await nextRender();
+ });
+
+ it('should generate items text content using itemLabelGenerator', async () => {
+ comboBox.open();
+ await nextRender();
+
+ const items = getAllItems(comboBox);
+ expect(items[0].textContent).to.equal('John Doe');
+ expect(items[1].textContent).to.equal('Jane Smith');
+ expect(items[2].textContent).to.equal('Bob Johnson');
+ });
+
+ it('should generate chips text content using itemLabelGenerator', async () => {
+ comboBox.selectedItems = [comboBox.items[0], comboBox.items[1]];
+ await nextRender();
+
+ const chips = getChips(comboBox);
+ expect(chips[1].label).to.equal('John Doe');
+ expect(chips[2].label).to.equal('Jane Smith');
+ });
+
+ it('should filter items using generated label', () => {
+ setInputValue(comboBox, 'john');
+
+ expect(comboBox.filteredItems.length).to.equal(2);
+ expect(comboBox.filteredItems[0]).to.deep.equal(comboBox.items[0]);
+ expect(comboBox.filteredItems[1]).to.deep.equal(comboBox.items[2]);
+ });
+
+ it('should use itemLabelGenerator over itemLabelPath', async () => {
+ comboBox.itemLabelPath = 'surname';
+ comboBox.itemLabelGenerator = (item) => item.name;
+ comboBox.open();
+ await nextRender();
+
+ const items = getAllItems(comboBox);
+ expect(items[0].textContent).to.equal('John');
+ expect(items[1].textContent).to.equal('Jane');
+ });
+
+ it('should accept empty string returned from itemLabelGenerator', async () => {
+ comboBox.itemLabelGenerator = (item) => (item.id === 2 ? '' : `${item.name} ${item.surname}`);
+ comboBox.open();
+ await nextRender();
+
+ const items = getAllItems(comboBox);
+ expect(items[0].textContent).to.equal('John Doe');
+ expect(items[1].textContent).to.equal('');
+ expect(items[2].textContent).to.equal('Bob Johnson');
+ });
+
+ it('should update dropdown items when itemLabelGenerator changes', async () => {
+ comboBox.open();
+ await nextRender();
+
+ expect(getFirstItem(comboBox).textContent).to.equal('John Doe');
+
+ comboBox.itemLabelGenerator = (item) => `${item.name} (${item.age})`;
+ await nextRender();
+
+ expect(getFirstItem(comboBox).textContent).to.equal('John (30)');
+ });
+
+ it('should update chips when itemLabelGenerator changes', async () => {
+ comboBox.selectedItems = [comboBox.items[0]];
+ await nextRender();
+
+ expect(getChips(comboBox)[1].label).to.equal('John Doe');
+
+ comboBox.itemLabelGenerator = (item) => `${item.name} (${item.age})`;
+ await nextRender();
+
+ expect(getChips(comboBox)[1].label).to.equal('John (30)');
+ });
+ });
});
diff --git a/packages/multi-select-combo-box/test/typings/multi-select-combo-box.types.ts b/packages/multi-select-combo-box/test/typings/multi-select-combo-box.types.ts
index 05c540aecde..97e0e15ff77 100644
--- a/packages/multi-select-combo-box/test/typings/multi-select-combo-box.types.ts
+++ b/packages/multi-select-combo-box/test/typings/multi-select-combo-box.types.ts
@@ -93,6 +93,7 @@ assertType(narrowedComboBox.filter);
assertType(narrowedComboBox.filteredItems);
assertType(narrowedComboBox.items);
assertType<(item: TestComboBoxItem) => string>(narrowedComboBox.itemClassNameGenerator);
+assertType<((item: TestComboBoxItem) => string) | undefined>(narrowedComboBox.itemLabelGenerator);
assertType(narrowedComboBox.itemIdPath);
assertType(narrowedComboBox.itemLabelPath);
assertType(narrowedComboBox.itemValuePath);