Skip to content
Merged
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
53 changes: 38 additions & 15 deletions dev/playground/combo-box.html
Original file line number Diff line number Diff line change
Expand Up @@ -59,24 +59,47 @@ <h2 class="heading">States</h2>
</vaadin-combo-box>
</section>

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

<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
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 @@ -29,6 +29,13 @@ export declare class ComboBoxItemsMixinClass<TItem> {
*/
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
Expand Down
17 changes: 17 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 @@ -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
Expand Down Expand Up @@ -122,6 +131,10 @@ export const ComboBoxItemsMixin = (superClass) =>
if (props.has('filter')) {
this._filterChanged(this.filter);
}

if (props.has('itemLabelGenerator')) {
this.requestContentUpdate();
}
}

/**
Expand Down Expand Up @@ -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() : '';
Expand Down
74 changes: 72 additions & 2 deletions packages/combo-box/test/items.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,6 @@ describe('items', () => {
});

describe('itemClassNameGenerator', () => {
let comboBox;

beforeEach(async () => {
comboBox = fixtureSync('<vaadin-combo-box></vaadin-combo-box>');
await nextRender();
Expand Down Expand Up @@ -194,4 +192,76 @@ describe('items', () => {
expect(items[2].className).to.equal('');
});
});

describe('itemLabelGenerator', () => {
beforeEach(async () => {
comboBox = fixtureSync('<vaadin-combo-box></vaadin-combo-box>');
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)');
});
});
});
1 change: 1 addition & 0 deletions packages/combo-box/test/typings/combo-box.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ assertType<string>(narrowedComboBox.filter);
assertType<TestComboBoxItem[] | undefined>(narrowedComboBox.filteredItems);
assertType<TestComboBoxItem[] | undefined>(narrowedComboBox.items);
assertType<(item: TestComboBoxItem) => string>(narrowedComboBox.itemClassNameGenerator);
assertType<((item: TestComboBoxItem) => string) | undefined>(narrowedComboBox.itemLabelGenerator);
assertType<string | null | undefined>(narrowedComboBox.itemIdPath);
assertType<string>(narrowedComboBox.itemLabelPath);
assertType<string>(narrowedComboBox.itemValuePath);
Expand Down
3 changes: 1 addition & 2 deletions packages/multi-select-combo-box/test/chips.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
18 changes: 18 additions & 0 deletions packages/multi-select-combo-box/test/helpers.js
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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');
97 changes: 92 additions & 5 deletions packages/multi-select-combo-box/test/items.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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('<vaadin-multi-select-combo-box></vaadin-multi-select-combo-box>');
await nextRender();
Expand Down Expand Up @@ -184,4 +180,95 @@ describe('items', () => {
expect(chips[2].className).to.equal('');
});
});

describe('itemLabelGenerator', () => {
beforeEach(async () => {
comboBox = fixtureSync(`
<vaadin-multi-select-combo-box
auto-expand-horizontally
item-id-path="id"
></vaadin-multi-select-combo-box>
`);
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)');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ assertType<string>(narrowedComboBox.filter);
assertType<TestComboBoxItem[] | undefined>(narrowedComboBox.filteredItems);
assertType<TestComboBoxItem[] | undefined>(narrowedComboBox.items);
assertType<(item: TestComboBoxItem) => string>(narrowedComboBox.itemClassNameGenerator);
assertType<((item: TestComboBoxItem) => string) | undefined>(narrowedComboBox.itemLabelGenerator);
assertType<string | null | undefined>(narrowedComboBox.itemIdPath);
assertType<string>(narrowedComboBox.itemLabelPath);
assertType<string>(narrowedComboBox.itemValuePath);
Expand Down