Skip to content

Commit f9b536a

Browse files
Artur-claude
andcommitted
feat(select): add itemLabelGenerator property
Adds a new itemLabelGenerator function property to vaadin-select that allows customizing how item labels are generated from item objects. This enables users to display custom text for items without modifying the underlying data structure. - Add itemLabelGenerator property to SelectBaseMixin - Update item rendering to use generator function when provided - Add TypeScript definitions for ItemLabelGenerator type - Add comprehensive tests for the new functionality - Include demo showing various use cases Fixes #8333 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent d5579d6 commit f9b536a

File tree

5 files changed

+279
-3
lines changed

5 files changed

+279
-3
lines changed
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<title>Vaadin Select - Item Label Generator Example</title>
6+
<script type="module">
7+
import '../src/vaadin-select.js';
8+
</script>
9+
<style>
10+
body {
11+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
12+
padding: 20px;
13+
}
14+
.example {
15+
margin-bottom: 30px;
16+
}
17+
h2 {
18+
margin-top: 30px;
19+
}
20+
</style>
21+
</head>
22+
<body>
23+
<h1>Vaadin Select - Item Label Generator Examples</h1>
24+
25+
<div class="example">
26+
<h2>Basic Example - Custom Display Labels</h2>
27+
<vaadin-select id="basic-select" label="Select a country"></vaadin-select>
28+
</div>
29+
30+
<div class="example">
31+
<h2>Complex Objects - User List</h2>
32+
<vaadin-select id="user-select" label="Select a user"></vaadin-select>
33+
</div>
34+
35+
<div class="example">
36+
<h2>Dynamic Label Generation</h2>
37+
<vaadin-select id="dynamic-select" label="Select an option"></vaadin-select>
38+
<button id="toggle-format">Toggle Label Format</button>
39+
</div>
40+
41+
<script type="module">
42+
// Basic Example - Custom Display Labels
43+
const basicSelect = document.querySelector('#basic-select');
44+
basicSelect.items = [
45+
{ code: 'US', name: 'United States', value: 'us' },
46+
{ code: 'GB', name: 'United Kingdom', value: 'gb' },
47+
{ code: 'FR', name: 'France', value: 'fr' },
48+
{ code: 'DE', name: 'Germany', value: 'de' },
49+
{ code: 'JP', name: 'Japan', value: 'jp' }
50+
];
51+
52+
// Generate labels showing both code and name
53+
basicSelect.itemLabelGenerator = (item) => `${item.code} - ${item.name}`;
54+
55+
// Complex Objects - User List
56+
const userSelect = document.querySelector('#user-select');
57+
userSelect.items = [
58+
{
59+
user: { firstName: 'John', lastName: 'Doe', email: '[email protected]' },
60+
department: 'Engineering',
61+
value: '1'
62+
},
63+
{
64+
user: { firstName: 'Jane', lastName: 'Smith', email: '[email protected]' },
65+
department: 'Marketing',
66+
value: '2'
67+
},
68+
{
69+
user: { firstName: 'Bob', lastName: 'Johnson', email: '[email protected]' },
70+
department: 'Sales',
71+
value: '3'
72+
}
73+
];
74+
75+
// Generate labels showing full name and department
76+
userSelect.itemLabelGenerator = (item) =>
77+
`${item.user.firstName} ${item.user.lastName} (${item.department})`;
78+
79+
// Dynamic Label Generation
80+
const dynamicSelect = document.querySelector('#dynamic-select');
81+
const toggleButton = document.querySelector('#toggle-format');
82+
83+
dynamicSelect.items = [
84+
{ label: 'Option A', value: 'a', priority: 'High', id: '001' },
85+
{ label: 'Option B', value: 'b', priority: 'Medium', id: '002' },
86+
{ label: 'Option C', value: 'c', priority: 'Low', id: '003' }
87+
];
88+
89+
let formatType = 'simple';
90+
91+
const updateLabelFormat = () => {
92+
if (formatType === 'simple') {
93+
dynamicSelect.itemLabelGenerator = (item) => item.label;
94+
} else if (formatType === 'detailed') {
95+
dynamicSelect.itemLabelGenerator = (item) => `[${item.id}] ${item.label} - Priority: ${item.priority}`;
96+
} else {
97+
dynamicSelect.itemLabelGenerator = (item) => `${item.priority.toUpperCase()}: ${item.label}`;
98+
}
99+
};
100+
101+
updateLabelFormat();
102+
103+
toggleButton.addEventListener('click', () => {
104+
if (formatType === 'simple') {
105+
formatType = 'detailed';
106+
} else if (formatType === 'detailed') {
107+
formatType = 'priority';
108+
} else {
109+
formatType = 'simple';
110+
}
111+
updateLabelFormat();
112+
});
113+
</script>
114+
</body>
115+
</html>

packages/select/src/vaadin-select-base-mixin.d.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import type { DelegateStateMixinClass } from '@vaadin/component-base/src/delegat
1313
import type { FieldMixinClass } from '@vaadin/field-base/src/field-mixin.js';
1414
import type { LabelMixinClass } from '@vaadin/field-base/src/label-mixin.js';
1515
import type { ValidateMixinClass } from '@vaadin/field-base/src/validate-mixin.js';
16-
import type { SelectItem, SelectRenderer } from './vaadin-select.js';
16+
import type { ItemLabelGenerator, SelectItem, SelectRenderer } from './vaadin-select.js';
1717

1818
export declare function SelectBaseMixin<T extends Constructor<HTMLElement>>(
1919
base: T,
@@ -80,6 +80,12 @@ export declare class SelectBaseMixinClass {
8080
*/
8181
value: string;
8282

83+
/**
84+
* A function to generate the label for each item based on the item.
85+
* The function receives the item as an argument and should return a string.
86+
*/
87+
itemLabelGenerator: ItemLabelGenerator | undefined;
88+
8389
/**
8490
* The name of this element.
8591
*/

packages/select/src/vaadin-select-base-mixin.js

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,17 @@ export const SelectBaseMixin = (superClass) =>
9696
sync: true,
9797
},
9898

99+
/**
100+
* A function to generate the label for each item based on the item.
101+
* The function receives the item as an argument and should return a string.
102+
*
103+
* @type {ItemLabelGenerator | undefined}
104+
*/
105+
itemLabelGenerator: {
106+
type: Function,
107+
observer: '__itemLabelGeneratorChanged',
108+
},
109+
99110
/**
100111
* The name of this element.
101112
*/
@@ -337,6 +348,12 @@ export const SelectBaseMixin = (superClass) =>
337348
}
338349
}
339350

351+
/** @private */
352+
__itemLabelGeneratorChanged() {
353+
// Request content update to re-render items with new labels
354+
this.requestContentUpdate();
355+
}
356+
340357
/**
341358
* @param {!KeyboardEvent} e
342359
* @protected
@@ -458,9 +475,13 @@ export const SelectBaseMixin = (superClass) =>
458475
*/
459476
__createItemElement(item) {
460477
const itemElement = document.createElement(item.component || 'vaadin-select-item');
461-
if (item.label) {
462-
itemElement.textContent = item.label;
478+
479+
// Use itemLabelGenerator if provided, otherwise use item.label
480+
const label = this.itemLabelGenerator ? this.itemLabelGenerator(item) : item.label;
481+
if (label) {
482+
itemElement.textContent = label;
463483
}
484+
464485
if (item.value) {
465486
itemElement.value = item.value;
466487
}

packages/select/src/vaadin-select.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ export type SelectChangeEvent = Event & {
3131
*/
3232
export type SelectRenderer = (root: HTMLElement, select: Select) => void;
3333

34+
/**
35+
* Function for generating the label for each item.
36+
* Receives one argument:
37+
*
38+
* - `item` The item object to generate a label for.
39+
*/
40+
export type ItemLabelGenerator = (item: SelectItem) => string;
41+
3442
/**
3543
* Fired when the `opened` property changes.
3644
*/
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import { expect } from '@vaadin/chai-plugins';
2+
import { fixtureSync, nextFrame, nextRender, oneEvent } from '@vaadin/testing-helpers';
3+
import '../src/vaadin-select.js';
4+
5+
describe('item-label-generator', () => {
6+
let select;
7+
8+
beforeEach(async () => {
9+
select = fixtureSync('<vaadin-select></vaadin-select>');
10+
await nextFrame();
11+
});
12+
13+
describe('basic functionality', () => {
14+
beforeEach(async () => {
15+
select.items = [
16+
{ label: 'Option 1', value: '1', customProp: 'Custom 1' },
17+
{ label: 'Option 2', value: '2', customProp: 'Custom 2' },
18+
{ label: 'Option 3', value: '3', customProp: 'Custom 3' },
19+
];
20+
await nextFrame();
21+
});
22+
23+
it('should use item label by default', async () => {
24+
select.opened = true;
25+
await oneEvent(select._overlayElement, 'vaadin-overlay-open');
26+
await nextRender();
27+
28+
const items = select._overlayElement.querySelectorAll('vaadin-select-item');
29+
expect(items[0].textContent).to.equal('Option 1');
30+
expect(items[1].textContent).to.equal('Option 2');
31+
expect(items[2].textContent).to.equal('Option 3');
32+
});
33+
34+
it('should use itemLabelGenerator when provided', async () => {
35+
select.itemLabelGenerator = (item) => `${item.customProp} - ${item.value}`;
36+
await nextFrame();
37+
38+
select.opened = true;
39+
await oneEvent(select._overlayElement, 'vaadin-overlay-open');
40+
await nextRender();
41+
42+
const items = select._overlayElement.querySelectorAll('vaadin-select-item');
43+
expect(items[0].textContent).to.equal('Custom 1 - 1');
44+
expect(items[1].textContent).to.equal('Custom 2 - 2');
45+
expect(items[2].textContent).to.equal('Custom 3 - 3');
46+
});
47+
48+
it('should update items when itemLabelGenerator changes', async () => {
49+
select.itemLabelGenerator = (item) => item.value;
50+
await nextFrame();
51+
52+
select.opened = true;
53+
await oneEvent(select._overlayElement, 'vaadin-overlay-open');
54+
await nextRender();
55+
56+
let items = select._overlayElement.querySelectorAll('vaadin-select-item');
57+
expect(items[0].textContent).to.equal('1');
58+
59+
select.itemLabelGenerator = (item) => `Value: ${item.value}`;
60+
await nextFrame();
61+
62+
items = select._overlayElement.querySelectorAll('vaadin-select-item');
63+
expect(items[0].textContent).to.equal('Value: 1');
64+
});
65+
66+
it('should handle undefined return from itemLabelGenerator', async () => {
67+
select.itemLabelGenerator = () => undefined;
68+
await nextFrame();
69+
70+
select.opened = true;
71+
await oneEvent(select._overlayElement, 'vaadin-overlay-open');
72+
await nextRender();
73+
74+
const items = select._overlayElement.querySelectorAll('vaadin-select-item');
75+
expect(items[0].textContent).to.equal('');
76+
});
77+
78+
it('should handle null return from itemLabelGenerator', async () => {
79+
select.itemLabelGenerator = () => null;
80+
await nextFrame();
81+
82+
select.opened = true;
83+
await oneEvent(select._overlayElement, 'vaadin-overlay-open');
84+
await nextRender();
85+
86+
const items = select._overlayElement.querySelectorAll('vaadin-select-item');
87+
expect(items[0].textContent).to.equal('');
88+
});
89+
});
90+
91+
describe('with complex items', () => {
92+
it('should work with nested object properties', async () => {
93+
select.items = [
94+
{ data: { name: 'John', age: 30 }, value: '1' },
95+
{ data: { name: 'Jane', age: 25 }, value: '2' },
96+
];
97+
98+
select.itemLabelGenerator = (item) => `${item.data.name} (${item.data.age})`;
99+
await nextFrame();
100+
101+
select.opened = true;
102+
await oneEvent(select._overlayElement, 'vaadin-overlay-open');
103+
await nextRender();
104+
105+
const items = select._overlayElement.querySelectorAll('vaadin-select-item');
106+
expect(items[0].textContent).to.equal('John (30)');
107+
expect(items[1].textContent).to.equal('Jane (25)');
108+
});
109+
});
110+
111+
describe('selected value display', () => {
112+
it('should show generated label for selected item', async () => {
113+
select.items = [
114+
{ label: 'Option 1', value: '1', displayName: 'First' },
115+
{ label: 'Option 2', value: '2', displayName: 'Second' },
116+
];
117+
118+
select.itemLabelGenerator = (item) => item.displayName;
119+
select.value = '1';
120+
await nextFrame();
121+
122+
const valueButton = select.querySelector('[slot="value"]');
123+
expect(valueButton.textContent.trim()).to.equal('First');
124+
});
125+
});
126+
});

0 commit comments

Comments
 (0)