diff --git a/packages/clay-autocomplete/src/ClayAutocomplete.js b/packages/clay-autocomplete/src/ClayAutocomplete.js index 508f818ea5..780c6e7105 100644 --- a/packages/clay-autocomplete/src/ClayAutocomplete.js +++ b/packages/clay-autocomplete/src/ClayAutocomplete.js @@ -70,7 +70,7 @@ class ClayAutocomplete extends ClayComponent { const item = this.filteredItems[Number(index)]; return !this.emit({ - data: item, + data: item.data, name: 'itemSelected', originalEvent: event, }); @@ -155,6 +155,7 @@ class ClayAutocomplete extends ClayComponent { data: { value: this.refs.input.value, key: event.key, + eventFromInput: event.delegateTarget.tagName === 'INPUT', }, name: 'inputOnKeydown', originalEvent: event, @@ -268,9 +269,11 @@ ClayAutocomplete.STATE = { * @instance * @default (elem) => elem * @memberof ClayAutocomplete - * @type {?(function|undefined)} + * @type {?(function|string)} */ - extractData: Config.func(), + extractData: Config.oneOfType([Config.func(), Config.string()]).value( + elem => elem + ), /** * List of filtered items for suggestion or autocomplete. diff --git a/packages/clay-autocomplete/src/__tests__/ClayAutocomplete.js b/packages/clay-autocomplete/src/__tests__/ClayAutocomplete.js index ae4d23fcf7..66f5b651cd 100644 --- a/packages/clay-autocomplete/src/__tests__/ClayAutocomplete.js +++ b/packages/clay-autocomplete/src/__tests__/ClayAutocomplete.js @@ -151,8 +151,9 @@ describe('ClayAutocomplete', function() { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ data: { - value: 'foo', + eventFromInput: true, key: 'o', + value: 'foo', }, name: 'inputOnKeydown', originalEvent: expect.any(Object), @@ -205,7 +206,7 @@ describe('ClayAutocomplete', function() { expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - data: expect.any(Object), + data: 'Bread', name: 'itemSelected', originalEvent: expect.any(Object), }) @@ -238,7 +239,7 @@ describe('ClayAutocomplete', function() { expect(spy).toHaveBeenCalled(); expect(spy).toHaveBeenCalledWith( expect.objectContaining({ - data: expect.any(Object), + data: 'Bread', name: 'itemSelected', originalEvent: expect.any(Object), }) @@ -262,7 +263,7 @@ describe('ClayAutocomplete', function() { expect(spy).not.toHaveBeenCalledWith( expect.objectContaining({ - data: expect.any(Object), + data: 'Bread', name: 'itemSelected', originalEvent: expect.any(Object), }) diff --git a/packages/clay-data-provider/src/ClayDataProvider.js b/packages/clay-data-provider/src/ClayDataProvider.js index 130848fd97..84c2f739fd 100644 --- a/packages/clay-data-provider/src/ClayDataProvider.js +++ b/packages/clay-data-provider/src/ClayDataProvider.js @@ -104,6 +104,25 @@ class ClayDataProvider extends ClayComponent { } } + /** + * Handles data mapping. + * @param {!(function|string)} param + * @param {!Array} data + * @protected + * @return {!(string|number)} + */ + _performCall(param, data) { + if (typeof param === 'function') { + return param(data); + } + + if (typeof data === 'string') { + return data; + } + + return data[param]; + } + /** * @inheritDoc */ @@ -131,7 +150,7 @@ class ClayDataProvider extends ClayComponent { /** * Helper method to filter a list based on a string. * @param {!string} query - * @param {?function} extract + * @param {?(function|string)} extract * @public * @return {Array} A list of items containing the corresponding characters */ @@ -142,15 +161,16 @@ class ClayDataProvider extends ClayComponent { return this._dataSource .reduce((prev, element, index) => { - let string = extract(element); + let string = this._performCall(extract, element); let result = match(query, string); if (result != null) { prev[prev.length] = { + data: element, index, matches: result.values, - originalString: string, score: result.score, + value: string, }; } diff --git a/packages/clay-data-provider/src/__tests__/ClayDataProvider.js b/packages/clay-data-provider/src/__tests__/ClayDataProvider.js index d351cca273..2f3b11acdc 100644 --- a/packages/clay-data-provider/src/__tests__/ClayDataProvider.js +++ b/packages/clay-data-provider/src/__tests__/ClayDataProvider.js @@ -75,6 +75,7 @@ describe('ClayDataProvider', function() { expect(filteredItem).toEqual([ { + data: 'Bread', index: 0, matches: [ {match: true, value: 'B'}, @@ -83,7 +84,7 @@ describe('ClayDataProvider', function() { {match: true, value: 'a'}, {value: 'd'}, ], - originalString: 'Bread', + value: 'Bread', score: 26, }, ]); diff --git a/packages/clay-multi-select/demos/a11y.html b/packages/clay-multi-select/demos/a11y.html index d9a860165a..8f57d6483a 100644 --- a/packages/clay-multi-select/demos/a11y.html +++ b/packages/clay-multi-select/demos/a11y.html @@ -33,15 +33,8 @@

Default State

-

With autocomplete

-
-
-
- -
-
-

With autocomplete and interaction

-
+

With disabled autocomplete

+
@@ -89,73 +82,32 @@

With only data remote

// Default State new metal.ClayMultiSelect({ - data: dataSource, + dataSource, helpText: 'You can use a comma to enter tags', label: 'Tags', selectedItems, spritemap, }, '#default-block'); - // With autocomplete + // With disabled autocomplete new metal.ClayMultiSelect({ - data: dataSource, + dataSource: [], + enableAutocomplete: false, helpText: 'You can use a comma to enter tags', label: 'Tags', - selectedItems, - spritemap, - }, '#autocomplete-block'); - - // With autocomplete and interaction - new metal.ClayMultiSelect({ - data: dataSource, - label: 'Tags', - selectedItems, spritemap, - events: { - itemAdded: (event) => { - event.target.selectedItems.push({label: event.data.label, value: event.data.label}); - addToSelectedItems(event); - }, - itemRemoved: removeItem, - itemSelected: (event) => { - const label = event.data.originalString.toLowerCase(); - if (event.target.selectedItems.find(item => item.value === label)) return; - event.target.selectedItems.push({label, value: label}); - addToSelectedItems(event); - }, - }, - helpText: 'You can use a comma to enter tags', - }, '#interaction-block'); + }, '#disabled-autocomplete-block'); // With only data remote new metal.ClayMultiSelect({ - data: 'https://api.pro.coinbase.com/currencies', - events: { - itemRemoved: removeItem, - itemSelected: (event) => { - const label = event.data.originalString.toLowerCase(); - if (event.target.selectedItems.find(item => item.value === label)) return; - event.target.selectedItems.push({label, value: label}); - addToSelectedItems(event); - }, - }, - extractData: (elem) => elem.name, + dataSource: 'https://api.pro.coinbase.com/currencies', helpText: 'You can use a comma to enter tags', label: 'Currencies', + labelLocator: 'name', spritemap, + valueLocator: 'id', }, '#selected-block'); - function addToSelectedItems(event) { - event.target.selectedItems = event.target.selectedItems; - event.target.clearInput(); - event.target.filteredItems = []; - } - - function removeItem(event) { - event.target.selectedItems.splice(event.data.index, 1); - event.target.selectedItems = event.target.selectedItems; - } - diff --git a/packages/clay-multi-select/demos/index.html b/packages/clay-multi-select/demos/index.html index 89e0aa21bf..8f57d6483a 100644 --- a/packages/clay-multi-select/demos/index.html +++ b/packages/clay-multi-select/demos/index.html @@ -33,15 +33,8 @@

Default State

-

With autocomplete

-
-
-
- -
-
-

With autocomplete and interaction

-
+

With disabled autocomplete

+
@@ -96,66 +89,25 @@

With only data remote

spritemap, }, '#default-block'); - // With autocomplete + // With disabled autocomplete new metal.ClayMultiSelect({ - dataSource, + dataSource: [], + enableAutocomplete: false, helpText: 'You can use a comma to enter tags', label: 'Tags', - selectedItems, - spritemap, - }, '#autocomplete-block'); - - // With autocomplete and interaction - new metal.ClayMultiSelect({ - dataSource, - label: 'Tags', - selectedItems, spritemap, - events: { - itemAdded: (event) => { - event.target.selectedItems.push({label: event.data.label, value: event.data.label}); - addToSelectedItems(event); - }, - itemRemoved: removeItem, - itemSelected: (event) => { - const label = event.data.originalString.toLowerCase(); - if (event.target.selectedItems.find(item => item.value === label)) return; - event.target.selectedItems.push({label, value: label}); - addToSelectedItems(event); - }, - }, - helpText: 'You can use a comma to enter tags', - }, '#interaction-block'); + }, '#disabled-autocomplete-block'); // With only data remote new metal.ClayMultiSelect({ dataSource: 'https://api.pro.coinbase.com/currencies', - events: { - itemRemoved: removeItem, - itemSelected: (event) => { - const label = event.data.originalString.toLowerCase(); - if (event.target.selectedItems.find(item => item.value === label)) return; - event.target.selectedItems.push({label, value: label}); - addToSelectedItems(event); - }, - }, - extractData: (elem) => elem.name, helpText: 'You can use a comma to enter tags', label: 'Currencies', + labelLocator: 'name', spritemap, + valueLocator: 'id', }, '#selected-block'); - function addToSelectedItems(event) { - event.target.selectedItems = event.target.selectedItems; - event.target.inputValue = ''; - event.target.filteredItems = []; - } - - function removeItem(event) { - event.target.selectedItems.splice(event.data.index, 1); - event.target.selectedItems = event.target.selectedItems; - } - diff --git a/packages/clay-multi-select/src/ClayMultiSelect.js b/packages/clay-multi-select/src/ClayMultiSelect.js index 0a2205ae18..8c6db275de 100644 --- a/packages/clay-multi-select/src/ClayMultiSelect.js +++ b/packages/clay-multi-select/src/ClayMultiSelect.js @@ -13,6 +13,20 @@ import templates from './ClayMultiSelect.soy.js'; * @extends ClayComponent */ class ClayMultiSelect extends ClayComponent { + /** + * Assemble the schema of the item. + * @param {!string} label + * @param {!string} value + * @protected + * @return {!Object} + */ + _getItemSchema(label, value) { + return { + label: label, + value: value, + }; + } + /** * Continues the propagation of the Button clicked event. * @param {!Event} event @@ -54,14 +68,18 @@ class ClayMultiSelect extends ClayComponent { * Handle the click on the dropdown item and the propagation of the labelAdded event. * @param {!Event} event * @protected - * @return {Boolean} If the event has been prevented or not. + * @return {?Boolean} If the event has been prevented or not. */ _handleDropdownItemClick(event) { - return !this.emit({ - data: event.data, - name: 'itemSelected', - originalEvent: event, - }); + this.filteredItems = []; + this.inputValue = ''; + this.refs.autocomplete.refs.input.focus(); + + const value = this._performCall(this.valueLocator, event.data); + const label = this._performCall(this.labelLocator, event.data); + const data = this._getItemSchema(label, value); + + return this._handleItemAdded(value, data, event, 'itemSelected'); } /** @@ -82,24 +100,32 @@ class ClayMultiSelect extends ClayComponent { /** * Continues the propagation of the itemAdded event. + * @param {!String} value + * @param {!(object|array|string)} data * @param {!Event} event + * @param {?String} eventName * @protected - * @return {Boolean} If the event has been prevented or not. + * @return {?Boolean} If the event has been prevented or not. */ - _handleItemAdded(event) { - const label = event.data.value.toLowerCase().replace(',', ''); + _handleItemAdded(value, data, event, eventName = 'itemAdded') { + const label = value.toLowerCase().replace(',', ''); if ( label.trim() && !this.selectedItems.find( - itemSelected => itemSelected.label === label + itemSelected => + this._performCall(this.valueLocator, itemSelected) === label ) ) { + const index = this.selectedItems.push(data); + + this.selectedItems = this.selectedItems; + return !this.emit({ data: { - label, + item: this.selectedItems[index - 1], }, - name: 'itemAdded', + name: eventName, originalEvent: event, }); } else { @@ -160,12 +186,15 @@ class ClayMultiSelect extends ClayComponent { */ _handleItemRemoved(event) { const index = event.getAttribute('data-tag'); + const item = this.selectedItems[Number(index)]; this._removeFocusedItem(); + this.selectedItems.splice(Number(index), 1); + this.selectedItems = this.selectedItems; return !this.emit({ data: { - index: Number(index), + item, }, name: 'itemRemoved', originalEvent: event, @@ -179,15 +208,21 @@ class ClayMultiSelect extends ClayComponent { * @return {Boolean} If the event has been prevented or not. */ _handleOnInput(event) { + const value = event.data.value; + this._removeFocusedItem(); switch (event.data.char) { case ',': - return this._handleItemAdded(event); + return this._handleItemAdded( + value, + this._getItemSchema(value, value), + event + ); default: return !this.emit({ data: { - value: event.data.value, + value, }, name: 'queryChange', originalEvent: event, @@ -209,8 +244,12 @@ class ClayMultiSelect extends ClayComponent { event.preventDefault(); if (this._itemFocused) { return this._handleItemRemoved(this._itemFocused); - } else if (value) { - return this._handleItemAdded(event); + } else if (value && event.data.eventFromInput) { + return this._handleItemAdded( + value, + this._getItemSchema(value, value), + event + ); } break; case 'Backspace': @@ -235,6 +274,25 @@ class ClayMultiSelect extends ClayComponent { } } + /** + * Handles data mapping. + * @param {!(function|string)} param + * @param {!Array} data + * @protected + * @return {!(string|number)} + */ + _performCall(param, data) { + if (typeof param === 'function') { + return param(data); + } + + if (typeof data === 'string') { + return data; + } + + return data[param]; + } + /** * Removes the focus from the focused element. * @protected @@ -314,15 +372,6 @@ ClayMultiSelect.STATE = { */ enableAutocomplete: Config.bool().value(true), - /** - * Extracts from the data the item to be compared in autocomplete. - * @instance - * @default (elem) => elem - * @memberof ClayMultiSelect - * @type {?(function|undefined)} - */ - extractData: Config.func(), - /** * List of filtered items for suggestion or autocomplete. * @default [] @@ -431,6 +480,17 @@ ClayMultiSelect.STATE = { */ label: Config.string(), + /** + * Sets the name of the field to map the label of the item. + * @default label + * @instance + * @memberof ClayMultiSelect + * @type {?(function|string)} + */ + labelLocator: Config.oneOfType([Config.func(), Config.string()]).value( + 'label' + ), + /** * List of the selected Items. * @default [] @@ -457,6 +517,17 @@ ClayMultiSelect.STATE = { * @type {!string} */ spritemap: Config.string().required(), + + /** + * Sets the name of the field to map the value of the item. + * @default value + * @instance + * @memberof ClayMultiSelect + * @type {?(function|string)} + */ + valueLocator: Config.oneOfType([Config.func(), Config.string()]).value( + 'value' + ), }; defineWebComponent('clay-multi-select', ClayMultiSelect); diff --git a/packages/clay-multi-select/src/ClayMultiSelect.soy b/packages/clay-multi-select/src/ClayMultiSelect.soy index 42ce5decf6..14bbcd5be2 100644 --- a/packages/clay-multi-select/src/ClayMultiSelect.soy +++ b/packages/clay-multi-select/src/ClayMultiSelect.soy @@ -18,13 +18,13 @@ {@param? contentRenderer: string} {@param? elementClasses: string} {@param? enableAutocomplete: bool} - {@param? extractData: ?} {@param? filteredItems: list} {@param? id: string} {@param? initialData: []|list} {@param? inputName: string} {@param? inputValue: string} {@param? label: string} + {@param? labelLocator: any} {@param? requestOptions: [ method: string, mode: string, @@ -76,12 +76,12 @@ {param contentRenderer: $contentRenderer /} {param dataSource: $dataSource /} {param enableAutocomplete: $enableAutocomplete /} - {param extractData: $extractData /} {param filteredItems: $filteredItems /} {param helpText: $helpText /} {param initialData: $initialData /} {param inputName: $inputName /} {param inputValue: $inputValue /} + {param labelLocator: $labelLocator /} {param requestOptions: $requestOptions /} {param requestPolling: $requestPolling /} {param requestRetries: $requestRetries /} @@ -110,11 +110,11 @@ {@param? _removeFocusedItem: any} {@param? contentRenderer: string} {@param? enableAutocomplete: bool} - {@param? extractData: ?} {@param? filteredItems: list} {@param? initialData: []|list} {@param? inputName: string} {@param? inputValue: string} + {@param? labelLocator: any} {@param? requestOptions: [ method: string, mode: string, @@ -147,7 +147,7 @@ 'itemSelected': $_handleDropdownItemClick, 'queryChange': $_handleOnInput ] /} - {param extractData: $extractData /} + {param extractData: $labelLocator /} {param filteredItems: $filteredItems /} {param initialData: $initialData /} {param inputElementClasses: 'form-control-inset' /} diff --git a/packages/clay-multi-select/src/__tests__/ClayMultiSelect.js b/packages/clay-multi-select/src/__tests__/ClayMultiSelect.js index b4a31f079a..45a0d666c1 100644 --- a/packages/clay-multi-select/src/__tests__/ClayMultiSelect.js +++ b/packages/clay-multi-select/src/__tests__/ClayMultiSelect.js @@ -270,7 +270,10 @@ describe('ClayMultiSelect', function() { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ data: { - label: 'foo', + item: { + label: 'foo', + value: 'foo', + }, }, name: 'itemAdded', originalEvent: expect.any(Object), @@ -297,7 +300,10 @@ describe('ClayMultiSelect', function() { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ data: { - label: 'bar', + item: { + label: 'bar', + value: 'bar', + }, }, name: 'itemAdded', originalEvent: expect.any(Object), @@ -330,7 +336,10 @@ describe('ClayMultiSelect', function() { expect(spy).not.toHaveBeenCalledWith( expect.objectContaining({ data: { - label: 'foo', + item: { + label: 'foo', + value: 'foo', + }, }, name: 'itemAdded', originalEvent: expect.any(Object), @@ -367,7 +376,10 @@ describe('ClayMultiSelect', function() { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ data: { - index: 1, + item: { + label: 'Bar', + value: 'bar', + }, }, name: 'itemRemoved', originalEvent: expect.any(Object), @@ -415,7 +427,10 @@ describe('ClayMultiSelect', function() { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ data: { - index: 1, + item: { + label: 'Bar', + value: 'bar', + }, }, name: 'itemRemoved', originalEvent: expect.any(Object), @@ -461,7 +476,10 @@ describe('ClayMultiSelect', function() { expect(spy).toHaveBeenCalledWith( expect.objectContaining({ data: { - index: 1, + item: { + label: 'Bar', + value: 'bar', + }, }, name: 'itemRemoved', originalEvent: expect.any(Object),