Skip to content

Commit 64377b6

Browse files
Artur-claude
andcommitted
feat: add itemLabelGenerator property to select, combo-box and multi-select-combo-box
Implement itemLabelGenerator function property that allows custom label generation for items. The generator function receives an item and returns a string label, taking precedence over itemLabelPath when both are defined. - Add itemLabelGenerator to vaadin-select for custom dropdown item labels - Add itemLabelGenerator to vaadin-combo-box with support for filtering by generated labels - Multi-select-combo-box inherits itemLabelGenerator for both dropdown and chip labels - Add comprehensive tests for all three components - Update dev examples to demonstrate the new feature Fixes #8333 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 2198726 commit 64377b6

File tree

8 files changed

+535
-24
lines changed

8 files changed

+535
-24
lines changed

dev/combo-box.html

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,16 @@ <h2 class="heading">Bells & Whistles</h2>
3434
</vaadin-combo-box>
3535
</section>
3636

37+
<section class="section">
38+
<h2 class="heading">Item Label Generator</h2>
39+
<vaadin-combo-box
40+
id="label-generator"
41+
label="Select product"
42+
helper-text="Products with custom label formatting"
43+
clear-button-visible
44+
></vaadin-combo-box>
45+
</section>
46+
3747
<section class="section">
3848
<h2 class="heading">States</h2>
3949
<vaadin-combo-box
@@ -61,22 +71,40 @@ <h2 class="heading">States</h2>
6171

6272
<script type="module">
6373
document.querySelectorAll('vaadin-combo-box').forEach((comboBox) => {
64-
comboBox.dataProvider = async (params, callback) => {
65-
const index = params.page * params.pageSize;
66-
const response = await fetch(
67-
`https://demo.vaadin.com/demo-data/1.0/filtered-countries?index=${index}&count=${params.pageSize}&filter=${params.filter}`,
68-
);
69-
if (response.ok) {
70-
const { result, size } = await response.json();
71-
// Emulate network latency for demo purpose
72-
setTimeout(() => {
73-
callback(result, size);
74-
}, 1000);
75-
}
76-
};
74+
if (comboBox.id === 'label-generator') {
75+
// Example with itemLabelGenerator
76+
comboBox.items = [
77+
{ id: 'p1', name: 'Laptop', category: 'Electronics', price: 999.99, inStock: true },
78+
{ id: 'p2', name: 'Mouse', category: 'Electronics', price: 29.99, inStock: true },
79+
{ id: 'p3', name: 'Keyboard', category: 'Electronics', price: 79.99, inStock: false },
80+
{ id: 'p4', name: 'Monitor', category: 'Electronics', price: 399.99, inStock: true },
81+
{ id: 'p5', name: 'Desk Chair', category: 'Furniture', price: 249.99, inStock: true },
82+
{ id: 'p6', name: 'Standing Desk', category: 'Furniture', price: 599.99, inStock: false },
83+
];
84+
comboBox.itemValuePath = 'id';
85+
comboBox.itemLabelGenerator = (item) => {
86+
const stockStatus = item.inStock ? '✅' : '❌';
87+
return `${item.name} - $${item.price} ${stockStatus}`;
88+
};
89+
comboBox.value = 'p1';
90+
} else {
91+
comboBox.dataProvider = async (params, callback) => {
92+
const index = params.page * params.pageSize;
93+
const response = await fetch(
94+
`https://demo.vaadin.com/demo-data/1.0/filtered-countries?index=${index}&count=${params.pageSize}&filter=${params.filter}`,
95+
);
96+
if (response.ok) {
97+
const { result, size } = await response.json();
98+
// Emulate network latency for demo purpose
99+
setTimeout(() => {
100+
callback(result, size);
101+
}, 1000);
102+
}
103+
};
77104

78-
if (!comboBox.placeholder) {
79-
comboBox.selectedItem = 'Andorra';
105+
if (!comboBox.placeholder) {
106+
comboBox.selectedItem = 'Andorra';
107+
}
80108
}
81109
});
82110
</script>

dev/multi-select-combo-box.html

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@
8484
</vaadin-multi-select-combo-box>
8585
</div>
8686

87+
<div style="margin-top: 2rem;">
88+
<h3>Object items with itemLabelGenerator</h3>
89+
<vaadin-multi-select-combo-box
90+
id="object-items"
91+
label="Select team members"
92+
placeholder="Search team members..."
93+
></vaadin-multi-select-combo-box>
94+
</div>
95+
8796
<script>
8897
const comboBox = document.querySelector('vaadin-multi-select-combo-box');
8998

@@ -152,6 +161,25 @@
152161
comboBox.setAttribute('dir', e.detail.value);
153162
});
154163

164+
// Setup object items example with itemLabelGenerator
165+
const objectItemsComboBox = document.getElementById('object-items');
166+
objectItemsComboBox.items = [
167+
{ id: 1, firstName: 'John', lastName: 'Doe', department: 'Engineering', role: 'Senior Developer' },
168+
{ id: 2, firstName: 'Jane', lastName: 'Smith', department: 'Design', role: 'UX Designer' },
169+
{ id: 3, firstName: 'Bob', lastName: 'Johnson', department: 'Engineering', role: 'DevOps Engineer' },
170+
{ id: 4, firstName: 'Alice', lastName: 'Williams', department: 'Product', role: 'Product Manager' },
171+
{ id: 5, firstName: 'Charlie', lastName: 'Brown', department: 'Engineering', role: 'Junior Developer' },
172+
{ id: 6, firstName: 'Diana', lastName: 'Prince', department: 'Design', role: 'UI Designer' },
173+
{ id: 7, firstName: 'Edward', lastName: 'Norton', department: 'Sales', role: 'Account Executive' },
174+
{ id: 8, firstName: 'Fiona', lastName: 'Green', department: 'Marketing', role: 'Marketing Manager' },
175+
];
176+
objectItemsComboBox.itemIdPath = 'id';
177+
objectItemsComboBox.itemLabelGenerator = (item) => `${item.firstName} ${item.lastName} (${item.department} - ${item.role})`;
178+
objectItemsComboBox.selectedItems = [
179+
objectItemsComboBox.items[0],
180+
objectItemsComboBox.items[2],
181+
];
182+
155183
// Event listeners for changes
156184
comboBox.addEventListener('custom-value-set', (event) => {
157185
const item = event.detail;

dev/select.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ <h2 class="heading">Custom renderer</h2>
3939
<vaadin-select id="custom" value="s"></vaadin-select>
4040
</section>
4141

42+
<section class="section">
43+
<h2 class="heading">Item Label Generator</h2>
44+
<vaadin-select id="label-generator" label="Select user" value="2"></vaadin-select>
45+
</section>
46+
4247
<section class="section">
4348
<h2 class="heading">States</h2>
4449
<vaadin-select
@@ -85,6 +90,15 @@ <h2 class="heading">States</h2>
8590
root,
8691
);
8792
};
93+
} else if (select.id === 'label-generator') {
94+
// Example with itemLabelGenerator
95+
select.items = [
96+
{ value: '1', firstName: 'John', lastName: 'Doe', role: 'Admin' },
97+
{ value: '2', firstName: 'Jane', lastName: 'Smith', role: 'User' },
98+
{ value: '3', firstName: 'Bob', lastName: 'Johnson', role: 'Manager' },
99+
{ value: '4', firstName: 'Alice', lastName: 'Williams', role: 'User' },
100+
];
101+
select.itemLabelGenerator = (item) => `${item.firstName} ${item.lastName} (${item.role})`;
88102
} else {
89103
select.items = [
90104
{ label: 'Show all', value: 'all' },

packages/combo-box/src/vaadin-combo-box-items-mixin.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,18 @@ export const ComboBoxItemsMixin = (superClass) =>
109109
value: 'value',
110110
sync: true,
111111
},
112+
113+
/**
114+
* Function that is used to generate the label for each item.
115+
* Receives one argument:
116+
* - `item` The item to generate the label for.
117+
*
118+
* @type {(item: ComboBoxItem) => string}
119+
*/
120+
itemLabelGenerator: {
121+
type: Function,
122+
observer: '_itemLabelGeneratorChanged',
123+
},
112124
};
113125
}
114126

@@ -161,6 +173,11 @@ export const ComboBoxItemsMixin = (superClass) =>
161173
* @override
162174
*/
163175
_getItemLabel(item) {
176+
// Use itemLabelGenerator if available
177+
if (this.itemLabelGenerator && item) {
178+
return this.itemLabelGenerator(item) || '';
179+
}
180+
164181
let label = item && this.itemLabelPath ? get(this.itemLabelPath, item) : undefined;
165182
if (label === undefined || label === null) {
166183
label = item ? item.toString() : '';
@@ -184,6 +201,13 @@ export const ComboBoxItemsMixin = (superClass) =>
184201
}
185202
}
186203

204+
/** @private */
205+
_itemLabelGeneratorChanged() {
206+
if (this._scroller) {
207+
this._scroller.requestContentUpdate();
208+
}
209+
}
210+
187211
/** @private */
188212
_filterChanged(filter) {
189213
// Scroll to the top of the list whenever the filter changes.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { expect } from '@vaadin/chai-plugins';
2+
import { fixtureSync, nextRender } from '@vaadin/testing-helpers';
3+
import '../vaadin-combo-box.js';
4+
import { setInputValue } from './helpers.js';
5+
6+
describe('item-label-generator', () => {
7+
let comboBox;
8+
9+
beforeEach(async () => {
10+
comboBox = fixtureSync('<vaadin-combo-box></vaadin-combo-box>');
11+
await nextRender();
12+
});
13+
14+
describe('basic functionality', () => {
15+
beforeEach(() => {
16+
comboBox.items = [
17+
{ id: 1, name: 'John', surname: 'Doe', age: 30 },
18+
{ id: 2, name: 'Jane', surname: 'Smith', age: 25 },
19+
{ id: 3, name: 'Bob', surname: 'Johnson', age: 35 },
20+
{ id: 4, name: 'Alice', surname: 'Williams', age: 28 },
21+
];
22+
});
23+
24+
it('should generate labels using itemLabelGenerator', async () => {
25+
comboBox.itemLabelGenerator = (item) => `${item.name} ${item.surname}`;
26+
comboBox.opened = true;
27+
await nextRender();
28+
29+
const items = comboBox._scroller.querySelectorAll('vaadin-combo-box-item');
30+
expect(items[0].textContent).to.equal('John Doe');
31+
expect(items[1].textContent).to.equal('Jane Smith');
32+
expect(items[2].textContent).to.equal('Bob Johnson');
33+
expect(items[3].textContent).to.equal('Alice Williams');
34+
});
35+
36+
it('should display generated label in input when item is selected', async () => {
37+
comboBox.itemLabelGenerator = (item) => `${item.name} ${item.surname} (${item.age})`;
38+
comboBox.itemValuePath = 'id';
39+
comboBox.value = 2;
40+
await nextRender();
41+
42+
expect(comboBox.inputElement.value).to.equal('Jane Smith (25)');
43+
});
44+
45+
it('should filter items using generated labels', () => {
46+
comboBox.itemLabelGenerator = (item) => `${item.name} ${item.surname}`;
47+
48+
setInputValue(comboBox, 'john');
49+
50+
expect(comboBox.filteredItems.length).to.equal(2);
51+
expect(comboBox.filteredItems[0]).to.deep.equal({ id: 1, name: 'John', surname: 'Doe', age: 30 });
52+
expect(comboBox.filteredItems[1]).to.deep.equal({ id: 3, name: 'Bob', surname: 'Johnson', age: 35 });
53+
});
54+
55+
it('should use itemLabelGenerator over itemLabelPath', async () => {
56+
comboBox.items = [
57+
{ id: 1, label: 'Label from path', customLabel: 'Custom Label 1' },
58+
{ id: 2, label: 'Another label', customLabel: 'Custom Label 2' },
59+
];
60+
comboBox.itemLabelPath = 'label';
61+
comboBox.itemLabelGenerator = (item) => item.customLabel;
62+
comboBox.opened = true;
63+
await nextRender();
64+
65+
const items = comboBox._scroller.querySelectorAll('vaadin-combo-box-item');
66+
expect(items[0].textContent).to.equal('Custom Label 1');
67+
expect(items[1].textContent).to.equal('Custom Label 2');
68+
});
69+
70+
it('should handle empty return from itemLabelGenerator', async () => {
71+
comboBox.itemLabelGenerator = (item) => {
72+
if (item.id === 2) {
73+
return '';
74+
}
75+
return `${item.name} ${item.surname}`;
76+
};
77+
comboBox.opened = true;
78+
await nextRender();
79+
80+
const items = comboBox._scroller.querySelectorAll('vaadin-combo-box-item');
81+
expect(items[0].textContent).to.equal('John Doe');
82+
expect(items[1].textContent).to.equal('');
83+
expect(items[2].textContent).to.equal('Bob Johnson');
84+
});
85+
86+
it('should update dropdown when itemLabelGenerator changes', async () => {
87+
comboBox.itemLabelGenerator = (item) => item.name;
88+
comboBox.opened = true;
89+
await nextRender();
90+
91+
let items = comboBox._scroller.querySelectorAll('vaadin-combo-box-item');
92+
expect(items[0].textContent).to.equal('John');
93+
94+
comboBox.itemLabelGenerator = (item) => `${item.name} (${item.age})`;
95+
await nextRender();
96+
97+
items = comboBox._scroller.querySelectorAll('vaadin-combo-box-item');
98+
expect(items[0].textContent).to.equal('John (30)');
99+
});
100+
101+
it('should work with string items when itemLabelGenerator is not set', async () => {
102+
comboBox.items = ['Apple', 'Banana', 'Orange'];
103+
comboBox.opened = true;
104+
await nextRender();
105+
106+
const items = comboBox._scroller.querySelectorAll('vaadin-combo-box-item');
107+
expect(items[0].textContent).to.equal('Apple');
108+
expect(items[1].textContent).to.equal('Banana');
109+
expect(items[2].textContent).to.equal('Orange');
110+
});
111+
112+
it('should find items by generated label when typing', () => {
113+
comboBox.itemLabelGenerator = (item) => `${item.surname}, ${item.name}`;
114+
comboBox.itemValuePath = 'id';
115+
116+
setInputValue(comboBox, 'Smith, Jane');
117+
118+
expect(comboBox.filteredItems.length).to.equal(1);
119+
expect(comboBox.filteredItems[0]).to.deep.equal({ id: 2, name: 'Jane', surname: 'Smith', age: 25 });
120+
});
121+
});
122+
});

0 commit comments

Comments
 (0)