Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 43 additions & 15 deletions dev/combo-box.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,16 @@ <h2 class="heading">Bells & Whistles</h2>
</vaadin-combo-box>
</section>

<section class="section">
<h2 class="heading">Item Label Generator</h2>
<vaadin-combo-box
id="label-generator"
label="Select product"
helper-text="Products with custom label formatting"
clear-button-visible
></vaadin-combo-box>
</section>

<section class="section">
<h2 class="heading">States</h2>
<vaadin-combo-box
Expand Down Expand Up @@ -61,22 +71,40 @@ <h2 class="heading">States</h2>

<script type="module">
document.querySelectorAll('vaadin-combo-box').forEach((comboBox) => {
comboBox.dataProvider = async (params, callback) => {
const index = params.page * params.pageSize;
const response = await fetch(
`https://demo.vaadin.com/demo-data/1.0/filtered-countries?index=${index}&count=${params.pageSize}&filter=${params.filter}`,
);
if (response.ok) {
const { result, size } = await response.json();
// Emulate network latency for demo purpose
setTimeout(() => {
callback(result, size);
}, 1000);
}
};
if (comboBox.id === 'label-generator') {
// Example with itemLabelGenerator
comboBox.items = [
{ id: 'p1', name: 'Laptop', category: 'Electronics', price: 999.99, inStock: true },
{ id: 'p2', name: 'Mouse', category: 'Electronics', price: 29.99, inStock: true },
{ id: 'p3', name: 'Keyboard', category: 'Electronics', price: 79.99, inStock: false },
{ id: 'p4', name: 'Monitor', category: 'Electronics', price: 399.99, inStock: true },
{ id: 'p5', name: 'Desk Chair', category: 'Furniture', price: 249.99, inStock: true },
{ id: 'p6', name: 'Standing Desk', category: 'Furniture', price: 599.99, inStock: false },
];
comboBox.itemValuePath = 'id';
comboBox.itemLabelGenerator = (item) => {
const stockStatus = item.inStock ? '✅' : '❌';
return `${item.name} - $${item.price} ${stockStatus}`;
};
comboBox.value = 'p1';
} else {
comboBox.dataProvider = async (params, callback) => {
const index = params.page * params.pageSize;
const response = await fetch(
`https://demo.vaadin.com/demo-data/1.0/filtered-countries?index=${index}&count=${params.pageSize}&filter=${params.filter}`,
);
if (response.ok) {
const { result, size } = await response.json();
// Emulate network latency for demo purpose
setTimeout(() => {
callback(result, size);
}, 1000);
}
};

if (!comboBox.placeholder) {
comboBox.selectedItem = 'Andorra';
if (!comboBox.placeholder) {
comboBox.selectedItem = 'Andorra';
}
}
});
</script>
Expand Down
26 changes: 26 additions & 0 deletions dev/multi-select-combo-box.html
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,15 @@
</vaadin-multi-select-combo-box>
</div>

<div style="margin-top: 2rem">
<h3>Object items with itemLabelGenerator</h3>
<vaadin-multi-select-combo-box
id="object-items"
label="Select team members"
placeholder="Search team members..."
></vaadin-multi-select-combo-box>
</div>

<script>
const comboBox = document.querySelector('vaadin-multi-select-combo-box');

Expand Down Expand Up @@ -152,6 +161,23 @@
comboBox.setAttribute('dir', e.detail.value);
});

// Setup object items example with itemLabelGenerator
const objectItemsComboBox = document.getElementById('object-items');
objectItemsComboBox.items = [
{ id: 1, firstName: 'John', lastName: 'Doe', department: 'Engineering', role: 'Senior Developer' },
{ id: 2, firstName: 'Jane', lastName: 'Smith', department: 'Design', role: 'UX Designer' },
{ id: 3, firstName: 'Bob', lastName: 'Johnson', department: 'Engineering', role: 'DevOps Engineer' },
{ id: 4, firstName: 'Alice', lastName: 'Williams', department: 'Product', role: 'Product Manager' },
{ id: 5, firstName: 'Charlie', lastName: 'Brown', department: 'Engineering', role: 'Junior Developer' },
{ id: 6, firstName: 'Diana', lastName: 'Prince', department: 'Design', role: 'UI Designer' },
{ id: 7, firstName: 'Edward', lastName: 'Norton', department: 'Sales', role: 'Account Executive' },
{ id: 8, firstName: 'Fiona', lastName: 'Green', department: 'Marketing', role: 'Marketing Manager' },
];
objectItemsComboBox.itemIdPath = 'id';
objectItemsComboBox.itemLabelGenerator = (item) =>
`${item.firstName} ${item.lastName} (${item.department} - ${item.role})`;
objectItemsComboBox.selectedItems = [objectItemsComboBox.items[0], objectItemsComboBox.items[2]];

// Event listeners for changes
comboBox.addEventListener('custom-value-set', (event) => {
const item = event.detail;
Expand Down
14 changes: 14 additions & 0 deletions dev/select.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ <h2 class="heading">Custom renderer</h2>
<vaadin-select id="custom" value="s"></vaadin-select>
</section>

<section class="section">
<h2 class="heading">Item Label Generator</h2>
<vaadin-select id="label-generator" label="Select user" value="2"></vaadin-select>
</section>

<section class="section">
<h2 class="heading">States</h2>
<vaadin-select
Expand Down Expand Up @@ -85,6 +90,15 @@ <h2 class="heading">States</h2>
root,
);
};
} else if (select.id === 'label-generator') {
// Example with itemLabelGenerator
select.items = [
{ value: '1', firstName: 'John', lastName: 'Doe', role: 'Admin' },
{ value: '2', firstName: 'Jane', lastName: 'Smith', role: 'User' },
{ value: '3', firstName: 'Bob', lastName: 'Johnson', role: 'Manager' },
{ value: '4', firstName: 'Alice', lastName: 'Williams', role: 'User' },
];
select.itemLabelGenerator = (item) => `${item.firstName} ${item.lastName} (${item.role})`;
} else {
select.items = [
{ label: 'Show all', value: 'all' },
Expand Down
7 changes: 7 additions & 0 deletions packages/combo-box/src/vaadin-combo-box-items-mixin.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,11 @@ export declare class ComboBoxItemsMixinClass<TItem> {
* @attr {string} item-value-path
*/
itemValuePath: string;

/**
* Function that is used to generate the label for each item.
* Receives one argument:
* - `item` The item to generate the label for.
*/
itemLabelGenerator: ((item: TItem) => string) | undefined;
}
24 changes: 24 additions & 0 deletions packages/combo-box/src/vaadin-combo-box-items-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,18 @@ export const ComboBoxItemsMixin = (superClass) =>
value: 'value',
sync: true,
},

/**
* Function that is used to generate the label for each item.
* Receives one argument:
* - `item` The item to generate the label for.
*
* @type {(item: ComboBoxItem) => string}
*/
itemLabelGenerator: {
type: Function,
observer: '_itemLabelGeneratorChanged',
},
};
}

Expand Down Expand Up @@ -161,6 +173,11 @@ export const ComboBoxItemsMixin = (superClass) =>
* @override
*/
_getItemLabel(item) {
// Use itemLabelGenerator if available
if (this.itemLabelGenerator && item) {
return this.itemLabelGenerator(item) || '';
}

let label = item && this.itemLabelPath ? get(this.itemLabelPath, item) : undefined;
if (label === undefined || label === null) {
label = item ? item.toString() : '';
Expand All @@ -184,6 +201,13 @@ export const ComboBoxItemsMixin = (superClass) =>
}
}

/** @private */
_itemLabelGeneratorChanged() {
if (this._scroller) {
this._scroller.requestContentUpdate();
}
}

/** @private */
_filterChanged(filter) {
// Scroll to the top of the list whenever the filter changes.
Expand Down
122 changes: 122 additions & 0 deletions packages/combo-box/test/item-label-generator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { expect } from '@vaadin/chai-plugins';
import { fixtureSync, nextRender } from '@vaadin/testing-helpers';
import '../vaadin-combo-box.js';
import { setInputValue } from './helpers.js';

describe('item-label-generator', () => {
let comboBox;

beforeEach(async () => {
comboBox = fixtureSync('<vaadin-combo-box></vaadin-combo-box>');
await nextRender();
});

describe('basic functionality', () => {
beforeEach(() => {
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 },
{ id: 4, name: 'Alice', surname: 'Williams', age: 28 },
];
});

it('should generate labels using itemLabelGenerator', async () => {
comboBox.itemLabelGenerator = (item) => `${item.name} ${item.surname}`;
comboBox.opened = true;
await nextRender();

const items = comboBox._scroller.querySelectorAll('vaadin-combo-box-item');
expect(items[0].textContent).to.equal('John Doe');
expect(items[1].textContent).to.equal('Jane Smith');
expect(items[2].textContent).to.equal('Bob Johnson');
expect(items[3].textContent).to.equal('Alice Williams');
});

it('should display generated label in input when item is selected', async () => {
comboBox.itemLabelGenerator = (item) => `${item.name} ${item.surname} (${item.age})`;
comboBox.itemValuePath = 'id';
comboBox.value = 2;
await nextRender();

expect(comboBox.inputElement.value).to.equal('Jane Smith (25)');
});

it('should filter items using generated labels', () => {
comboBox.itemLabelGenerator = (item) => `${item.name} ${item.surname}`;

setInputValue(comboBox, 'john');

expect(comboBox.filteredItems.length).to.equal(2);
expect(comboBox.filteredItems[0]).to.deep.equal({ id: 1, name: 'John', surname: 'Doe', age: 30 });
expect(comboBox.filteredItems[1]).to.deep.equal({ id: 3, name: 'Bob', surname: 'Johnson', age: 35 });
});

it('should use itemLabelGenerator over itemLabelPath', async () => {
comboBox.items = [
{ id: 1, label: 'Label from path', customLabel: 'Custom Label 1' },
{ id: 2, label: 'Another label', customLabel: 'Custom Label 2' },
];
comboBox.itemLabelPath = 'label';
comboBox.itemLabelGenerator = (item) => item.customLabel;
comboBox.opened = true;
await nextRender();

const items = comboBox._scroller.querySelectorAll('vaadin-combo-box-item');
expect(items[0].textContent).to.equal('Custom Label 1');
expect(items[1].textContent).to.equal('Custom Label 2');
});

it('should handle empty return from itemLabelGenerator', async () => {
comboBox.itemLabelGenerator = (item) => {
if (item.id === 2) {
return '';
}
return `${item.name} ${item.surname}`;
};
comboBox.opened = true;
await nextRender();

const items = comboBox._scroller.querySelectorAll('vaadin-combo-box-item');
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.itemLabelGenerator = (item) => item.name;
comboBox.opened = true;
await nextRender();

let items = comboBox._scroller.querySelectorAll('vaadin-combo-box-item');
expect(items[0].textContent).to.equal('John');

comboBox.itemLabelGenerator = (item) => `${item.name} (${item.age})`;
await nextRender();

items = comboBox._scroller.querySelectorAll('vaadin-combo-box-item');
expect(items[0].textContent).to.equal('John (30)');
});

it('should work with string items when itemLabelGenerator is not set', async () => {
comboBox.items = ['Apple', 'Banana', 'Orange'];
comboBox.opened = true;
await nextRender();

const items = comboBox._scroller.querySelectorAll('vaadin-combo-box-item');
expect(items[0].textContent).to.equal('Apple');
expect(items[1].textContent).to.equal('Banana');
expect(items[2].textContent).to.equal('Orange');
});

it('should find items by generated label when typing', () => {
comboBox.itemLabelGenerator = (item) => `${item.surname}, ${item.name}`;
comboBox.itemValuePath = 'id';

setInputValue(comboBox, 'Smith, Jane');

expect(comboBox.filteredItems.length).to.equal(1);
expect(comboBox.filteredItems[0]).to.deep.equal({ id: 2, name: 'Jane', surname: 'Smith', age: 25 });
});
});
});
Loading
Loading