diff --git a/.changeset/wet-crabs-shop.md b/.changeset/wet-crabs-shop.md
new file mode 100644
index 000000000..a260ed8f7
--- /dev/null
+++ b/.changeset/wet-crabs-shop.md
@@ -0,0 +1,5 @@
+---
+'@lion/combobox': minor
+---
+
+Add a new \_getTextboxValueFromOption method on LionCombobox, to be able to overide how the modelValue is displayed in the textbox
diff --git a/docs/components/inputs/combobox/features.md b/docs/components/inputs/combobox/features.md
index 5fe918228..65129b97b 100644
--- a/docs/components/inputs/combobox/features.md
+++ b/docs/components/inputs/combobox/features.md
@@ -15,6 +15,7 @@ availability of the popup.
```js script
import { LitElement, html, repeat } from '@lion/core';
import { listboxData } from '../../../../packages/listbox/docs/listboxData.js';
+import { LionCombobox } from '../../../../packages/combobox/src/LionCombobox.js';
import '@lion/listbox/define';
import '@lion/combobox/define';
import './src/demo-selection-display.js';
@@ -327,6 +328,49 @@ customElements.define('demo-server-side', DemoServerSide);
export const serverSideCompletion = () => html``;
```
+## Set complex object in choiceValue
+
+A common use case is to set a complex object in the `choiceValue` property. By default, `LionCombobox` will display in the text input the `modelValue`. By overriding the `_getTextboxValueFromOption` method, you can choose which value will be used for the input value.
+
+```js preview-story
+class ComplexObjectCombobox extends LionCombobox {
+ /**
+ * Override how the input text is filled
+ * @param {LionOption} option
+ * @returns {string}
+ */
+ // eslint-disable-next-line class-methods-use-this
+ _getTextboxValueFromOption(option) {
+ return option.label;
+ }
+}
+
+customElements.define('complex-object-combobox', ComplexObjectCombobox);
+
+const onModelValueChanged = event => {
+ console.log(`event.target.modelValue: ${JSON.stringify(event.target.modelValue)}`);
+};
+
+const listboxDataObject = listboxData.map(e => {
+ return { label: e, name: e };
+});
+
+export const complexObjectChoiceValue = () => html`
+ ${lazyRender(
+ listboxDataObject.map(
+ entry =>
+ html`
+ ${entry.label}
+ `,
+ ),
+ )}
+`;
+```
+
## Listbox compatibility
All configurations that can be applied to `lion-listbox`, can be applied to `lion-combobox` as well.
diff --git a/packages/combobox/src/LionCombobox.js b/packages/combobox/src/LionCombobox.js
index 4dd59dc24..53ed0beb4 100644
--- a/packages/combobox/src/LionCombobox.js
+++ b/packages/combobox/src/LionCombobox.js
@@ -320,7 +320,10 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
if (name === 'modelValue' && this.modelValue && this.modelValue !== oldValue) {
if (this._syncToTextboxCondition(this.modelValue, this._oldModelValue)) {
if (!this.multipleChoice) {
- this._setTextboxValue(this.modelValue);
+ const textboxValue = this._getTextboxValueFromOption(
+ this.formElements[/** @type {number} */ (this.checkedIndex)],
+ );
+ this._setTextboxValue(textboxValue);
} else {
this._syncToTextboxMultiple(this.modelValue, this._oldModelValue);
}
@@ -337,8 +340,12 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
*/
__unsyncCheckedIndexOnInputChange() {
const autoselect = this._autoSelectCondition();
- if (!this.multipleChoice && !autoselect && !this._inputNode.value.startsWith(this.modelValue)) {
- this.checkedIndex = -1;
+ const checkedElement = this.formElements[/** @type {number} */ (this.checkedIndex)];
+ if (!this.multipleChoice && !autoselect && checkedElement) {
+ const textboxValue = this._getTextboxValueFromOption(checkedElement);
+ if (!this._inputNode.value.startsWith(textboxValue)) {
+ this.checkedIndex = -1;
+ }
}
}
@@ -396,8 +403,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
*/
matchCondition(option, textboxValue) {
let idx = -1;
- if (typeof option.choiceValue === 'string' && typeof textboxValue === 'string') {
- idx = option.choiceValue.toLowerCase().indexOf(textboxValue.toLowerCase());
+ const inputValue = this._getTextboxValueFromOption(option);
+ if (typeof inputValue === 'string' && typeof textboxValue === 'string') {
+ idx = inputValue.toLowerCase().indexOf(textboxValue.toLowerCase());
}
if (this.matchMode === 'all') {
@@ -452,6 +460,17 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
return true;
}
+ /**
+ * Return the value to be used for the input value
+ * @overridable
+ * @param {LionOption} option
+ * @returns {string}
+ */
+ // eslint-disable-next-line class-methods-use-this
+ _getTextboxValueFromOption(option) {
+ return option.choiceValue;
+ }
+
/**
* @configure ListboxMixin whenever the options are changed (potentially due to external causes
* like server side filtering of nodes), schedule autocompletion for proper highlighting
@@ -516,8 +535,9 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
phase: 'overlay-close',
})
) {
- this._inputNode.value =
- this.formElements[/** @type {number} */ (this.checkedIndex)].choiceValue;
+ this._inputNode.value = this._getTextboxValueFromOption(
+ this.formElements[/** @type {number} */ (this.checkedIndex)],
+ );
}
} else {
this._syncToTextboxMultiple(this.modelValue, this._oldModelValue);
@@ -656,11 +676,10 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
// and _inputNode.value
if (isInlineAutoFillCandidate) {
- const stringValues =
- typeof option.choiceValue === 'string' && typeof curValue === 'string';
+ const textboxValue = this._getTextboxValueFromOption(option);
+ const stringValues = typeof textboxValue === 'string' && typeof curValue === 'string';
const beginsWith =
- stringValues &&
- option.choiceValue.toLowerCase().indexOf(curValue.toLowerCase()) === 0;
+ stringValues && textboxValue.toLowerCase().indexOf(curValue.toLowerCase()) === 0;
// We only can do proper inline autofilling when the beginning of the word matches
if (beginsWith) {
this.__textboxInlineComplete(option);
@@ -727,7 +746,7 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
*/
__textboxInlineComplete(option = this.formElements[this.activeIndex]) {
const prevLen = this._inputNode.value.length;
- this._inputNode.value = option.choiceValue;
+ this._inputNode.value = this._getTextboxValueFromOption(option);
this._inputNode.selectionStart = prevLen;
this._inputNode.selectionEnd = this._inputNode.value.length;
}
@@ -849,9 +868,14 @@ export class LionCombobox extends OverlayMixin(LionListbox) {
* @param {string[]} oldModelValue
* @protected
*/
+ // eslint-disable-next-line no-unused-vars
_syncToTextboxMultiple(modelValue, oldModelValue = []) {
- const diff = modelValue.filter(x => !oldModelValue.includes(x)).toString();
- this._setTextboxValue(diff); // or last selected value?
+ const diff = modelValue.filter(x => !oldModelValue.includes(x));
+ const newValue = this.formElements
+ .filter(option => diff.includes(option.choiceValue))
+ .map(option => this._getTextboxValueFromOption(option))
+ .join(' ');
+ this._setTextboxValue(newValue); // or last selected value?
}
/**
diff --git a/packages/combobox/test/lion-combobox.test.js b/packages/combobox/test/lion-combobox.test.js
index 8cccbd9c4..7d1b3230c 100644
--- a/packages/combobox/test/lion-combobox.test.js
+++ b/packages/combobox/test/lion-combobox.test.js
@@ -1680,6 +1680,211 @@ describe('lion-combobox', () => {
el.setCheckedIndex([0]);
expect(_inputNode.value).to.equal('Artichoke--multi');
});
+
+ describe('Override _getTextboxValueFromOption', () => {
+ it('allows to override "_getTextboxValueFromOption" and sync to textbox when multiple', async () => {
+ class X extends LionCombobox {
+ /**
+ * Return the value to be used for the input value
+ * @overridable
+ * @param {?} option
+ * @returns {string}
+ */
+ // eslint-disable-next-line class-methods-use-this
+ _getTextboxValueFromOption(option) {
+ return option.label;
+ }
+ }
+ const tagName = defineCE(X);
+ const tag = unsafeStatic(tagName);
+
+ const el = /** @type {LionCombobox} */ (
+ await fixture(html`
+ <${tag} name="foo" autocomplete="both" multiple-choice>
+ Artichoke
+ Chard
+ Chicory
+ Victoria Plum
+ ${tag}>
+ `)
+ );
+ const { _inputNode } = getComboboxMembers(el);
+ el.setCheckedIndex(-1);
+
+ // Act
+ el.setCheckedIndex([0]);
+
+ // Assert
+ expect(_inputNode.value).to.equal('Artichoke as label');
+
+ // Act
+ el.setCheckedIndex([3]);
+
+ // Assert
+ expect(_inputNode.value).to.equal('Victoria Plum as label');
+ });
+
+ it('allows to override "_getTextboxValueFromOption" and sync modelValue with textbox', async () => {
+ class X extends LionCombobox {
+ /**
+ * Return the value to be used for the input value
+ * @overridable
+ * @param {?} option
+ * @returns {string}
+ */
+ // eslint-disable-next-line class-methods-use-this
+ _getTextboxValueFromOption(option) {
+ return option.label;
+ }
+ }
+ const tagName = defineCE(X);
+ const tag = unsafeStatic(tagName);
+
+ const el = /** @type {LionCombobox} */ (
+ await fixture(html`
+ <${tag} name="foo">
+ Artichoke
+ Chard
+ Chicory
+ Victoria Plum
+ ${tag}>
+ `)
+ );
+ const { _inputNode } = getComboboxMembers(el);
+
+ // Assume
+ expect(_inputNode.value).to.equal('Chard as label');
+
+ // Act
+ el.modelValue = { value: 'Chicory' };
+ await el.updateComplete;
+
+ // Assert
+ expect(_inputNode.value).to.equal('Chicory as label');
+ });
+
+ it('allows to override "_getTextboxValueFromOption" and clears modelValue and textbox value on clear()', async () => {
+ class X extends LionCombobox {
+ /**
+ * Return the value to be used for the input value
+ * @overridable
+ * @param {?} option
+ * @returns {string}
+ */
+ // eslint-disable-next-line class-methods-use-this
+ _getTextboxValueFromOption(option) {
+ return option.label;
+ }
+ }
+ const tagName = defineCE(X);
+ const tag = unsafeStatic(tagName);
+
+ const el = /** @type {LionCombobox} */ (
+ await fixture(html`
+ <${tag} name="foo" .modelValue="${{ value: 'Artichoke' }}">
+ Artichoke
+ Chard
+ Chicory
+ Victoria Plum
+ ${tag}>
+ `)
+ );
+ const { _inputNode } = getComboboxMembers(el);
+
+ // Assume
+ expect(_inputNode.value).to.equal('Artichoke as label');
+
+ // Act
+ el.clear();
+
+ // Assert
+ expect(el.modelValue).to.equal('');
+ expect(_inputNode.value).to.equal('');
+ });
+
+ it('allows to override "_getTextboxValueFromOption" and syncs textbox to modelValue', async () => {
+ class X extends LionCombobox {
+ /**
+ * Return the value to be used for the input value
+ * @overridable
+ * @param {?} option
+ * @returns {string}
+ */
+ // eslint-disable-next-line class-methods-use-this
+ _getTextboxValueFromOption(option) {
+ return option.label;
+ }
+ }
+ const tagName = defineCE(X);
+ const tag = unsafeStatic(tagName);
+
+ const el = /** @type {LionCombobox} */ (
+ await fixture(html`
+ <${tag} name="foo">
+ Artichoke
+ Chard
+ Chicory
+ Victoria Plum
+ ${tag}>
+ `)
+ );
+ const { _inputNode } = getComboboxMembers(el);
+
+ async function performChecks() {
+ el.formElements[0].click();
+ await el.updateComplete;
+
+ // FIXME: fix properly for Webkit
+ // expect(_inputNode.value).to.equal('Aha');
+ expect(el.checkedIndex).to.equal(0);
+
+ mimicUserTyping(el, 'Arti');
+ await el.updateComplete;
+ expect(_inputNode.value).to.equal('Arti');
+
+ await el.updateComplete;
+ expect(el.checkedIndex).to.equal(-1);
+ }
+
+ el.autocomplete = 'none';
+ await performChecks();
+
+ el.autocomplete = 'list';
+ await performChecks();
+ });
+ });
});
describe('Active index behavior', () => {