From 279ba997a49ac163de30e658a91ec6aad706ac74 Mon Sep 17 00:00:00 2001 From: Travis Kaufman Date: Thu, 23 Feb 2017 16:45:39 -0500 Subject: [PATCH] feat(select): Add value retrieval mechanisms to JS API - Add `value` property to JS component to retrieve the value of the currently selected option - Add `getValue()` method to foundation in order to drive `value` property in component. - Add `getValueForOptionAtIndex` foundation method. - Enhance `verifyDefaultAdapter` foundation test helper by making it so that when checking for correct adapter methods, the method names don't have to be in the same order as they are defined on the object. BREAKING CHANGE: **New adapter method:** `getValueForOptionAtIndex(index: string) => string` should return the "value" of the option at the given index. Please add this method to your adapter implementations. Resolves #232 --- demos/select.html | 3 ++- packages/mdc-select/README.md | 12 ++++++++++-- packages/mdc-select/foundation.js | 5 +++++ packages/mdc-select/index.js | 5 +++++ test/unit/helpers/foundation.js | 3 ++- test/unit/mdc-select/foundation.test.js | 19 ++++++++++++++++++- test/unit/mdc-select/mdc-select.test.js | 21 +++++++++++++++++++++ 7 files changed, 63 insertions(+), 5 deletions(-) diff --git a/demos/select.html b/demos/select.html index 05a3445c417..49cc8fbc250 100644 --- a/demos/select.html +++ b/demos/select.html @@ -137,7 +137,8 @@

Custom Menu + Native Menu on mobile

root.addEventListener('MDCSelect:change', function() { var item = select.selectedOptions[0]; var index = select.selectedIndex; - currentlySelected.textContent = '"' + item.textContent + '" at index ' + index; + currentlySelected.textContent = '"' + item.textContent + '" at index ' + index + + ' with value "' + select.value + '"'; }); var demoWrapper = document.getElementById('demo-wrapper'); diff --git a/packages/mdc-select/README.md b/packages/mdc-select/README.md index a9a51c5e822..edf8159c41c 100644 --- a/packages/mdc-select/README.md +++ b/packages/mdc-select/README.md @@ -49,7 +49,8 @@ import {MDCSelect} from 'mdc-select'; const select = new MDCSelect(document.querySelector('.mdc-select')); select.listen('MDCSelect:change', () => { - alert(`Selected "${select.selectedOptions[0].textContent}" at index ${select.selectedIndex}"`); + alert(`Selected "${select.selectedOptions[0].textContent}" at index ${select.selectedIndex} ` + + `with value "${select.value}"`); }); ``` @@ -195,6 +196,7 @@ is outlined below. | Property Name | Type | Description | | --- | --- | --- | +| `value` | `string` | _(read-only)_ The `id` of the currently selected option. If no `id` is present on the selected option, its `textContent` is used. Returns an empty string when no option is selected. | | `options` | `HTMLElement[]` | _(read-only)_ An _array_ of menu items comprising the select's options. | | `selectedIndex` | `number` | The index of the currently selected option. Set to -1 if no option is currently selected. Changing this property will update the select element. | | `selectedOptions` | `HTMLElement[]` | _(read-only)_ A NodeList of either the currently selected option, or no elements if nothing is selected. | @@ -237,7 +239,7 @@ need it nonetheless. MDC Select ships with a foundation class that framework authors can use to integrate MDC Select into their custom components. Note that due to the nature of MDC Select, the adapter is quite complex. We try to provide as much guidance as possible, but we encourage developers to reach out -to use via GH Issues or on Gitter if they run into problems. +to us via GH Issues if they run into problems. ### Notes for component implementors @@ -285,6 +287,8 @@ within `componentDidUpdate`. | `setSelectedTextContent(selectedTextContent: string) => void` | Sets the text content of the `.mdc-select__selected-text` element to `selectedTextContent`. | | `getNumberOfOptions() => number` | Returns the number of options contained in the select's menu. | | `getTextForOptionAtIndex(index: number) => string` | Returns the text content for the option at the specified index within the select's menu. | +| `getValueForOptionAtIndex(index: number) => string` | Returns the value for the option at the specified index within the select's menu. We adhere to the conventions of `HTMLSelectElement` - +as described above - returning the value of the selected option's `id` in place of a `value` attribute and falling back to its `textContent`. Framework implementations may want to customize this method to suit their needs. | | `setAttrForOptionAtIndex(index: number, attr: string, value: string) => void` | Sets an attribute `attr` to value `value` for the option at the specified index within the select's menu. | | `rmAttrForOptionAtIndex(index: number, attr: string) => void` | Removes an attribute `attr` for the option at the specified index within the select's menu. | | `registerMenuInteractionHandler(type: string, handler: EventListener) => void` | Registers an event listener on the menu component's root element. Note that we will always listen for `MDCSimpleMenu:selected` for change events, and `MDCSimpleMenu:cancel` to know that we need to close the menu. If you are using a different events system, you could check the event type for either one of these strings and take the necessary steps to wire it up. | @@ -293,6 +297,10 @@ within `componentDidUpdate`. ### The full foundation API +#### MDCSelectFoundation.getValue() => string + +Returns the value of the currently selected option, or an empty string if no option is selected. + #### MDCSelectFoundation.getSelectedIndex() => number Returns the index of the currently selected option. Returns -1 if no option is currently selected. diff --git a/packages/mdc-select/foundation.js b/packages/mdc-select/foundation.js index 7dfc5c910d4..423bda451cb 100644 --- a/packages/mdc-select/foundation.js +++ b/packages/mdc-select/foundation.js @@ -59,6 +59,7 @@ export default class MDCSelectFoundation extends MDCFoundation { setSelectedTextContent: (/* textContent: string */) => {}, getNumberOfOptions: () => /* number */ 0, getTextForOptionAtIndex: (/* index: number */) => /* string */ '', + getValueForOptionAtIndex: (/* index: number */) => /* string */ '', setAttrForOptionAtIndex: (/* index: number, attr: string, value: string */) => {}, rmAttrForOptionAtIndex: (/* index: number, attr: string */) => {}, getOffsetTopForOptionAtIndex: (/* index: number */) => /* number */ 0, @@ -114,6 +115,10 @@ export default class MDCSelectFoundation extends MDCFoundation { this.adapter_.deregisterMenuInteractionHandler('MDCSimpleMenu:cancel', this.cancelHandler_); } + getValue() { + return this.selectedIndex_ >= 0 ? this.adapter_.getValueForOptionAtIndex(this.selectedIndex_) : ''; + } + getSelectedIndex() { return this.selectedIndex_; } diff --git a/packages/mdc-select/index.js b/packages/mdc-select/index.js index 3c397113f85..1376d8bb4ef 100644 --- a/packages/mdc-select/index.js +++ b/packages/mdc-select/index.js @@ -26,6 +26,10 @@ export class MDCSelect extends MDCComponent { return new MDCSelect(root); } + get value() { + return this.foundation_.getValue(); + } + get options() { return this.menu_.items; } @@ -100,6 +104,7 @@ export class MDCSelect extends MDCComponent { }, getNumberOfOptions: () => this.options.length, getTextForOptionAtIndex: (index) => this.options[index].textContent, + getValueForOptionAtIndex: (index) => this.options[index].id || this.options[index].textContent, setAttrForOptionAtIndex: (index, attr, value) => this.options[index].setAttribute(attr, value), rmAttrForOptionAtIndex: (index, attr) => this.options[index].removeAttribute(attr), getOffsetTopForOptionAtIndex: (index) => this.options[index].offsetTop, diff --git a/test/unit/helpers/foundation.js b/test/unit/helpers/foundation.js index 3d8963eac91..dd2b246d820 100644 --- a/test/unit/helpers/foundation.js +++ b/test/unit/helpers/foundation.js @@ -27,7 +27,8 @@ export function verifyDefaultAdapter(FoundationClass, expectedMethods) { const methods = Object.keys(defaultAdapter).filter((k) => typeof defaultAdapter[k] === 'function'); assert.equal(methods.length, Object.keys(defaultAdapter).length, 'Every adapter key must be a function'); - assert.deepEqual(methods, expectedMethods); + // Test for equality without requiring that the array be in a specific order + assert.deepEqual(methods.slice().sort(), expectedMethods.slice().sort()); // Test default methods methods.forEach((m) => assert.doesNotThrow(defaultAdapter[m])); } diff --git a/test/unit/mdc-select/foundation.test.js b/test/unit/mdc-select/foundation.test.js index 223a20de4ae..34f8f06ed00 100644 --- a/test/unit/mdc-select/foundation.test.js +++ b/test/unit/mdc-select/foundation.test.js @@ -37,7 +37,7 @@ test('default adapter returns a complete adapter implementation', () => { 'isMenuOpen', 'setSelectedTextContent', 'getNumberOfOptions', 'getTextForOptionAtIndex', 'setAttrForOptionAtIndex', 'rmAttrForOptionAtIndex', 'getOffsetTopForOptionAtIndex', 'registerMenuInteractionHandler', 'deregisterMenuInteractionHandler', 'notifyChange', - 'getWindowInnerHeight', + 'getWindowInnerHeight', 'getValueForOptionAtIndex', ]); }); @@ -226,3 +226,20 @@ test('#destroy deregisters all events registered within init()', () => { ); }); }); + +test('#getValue() returns the value of the option at the selected index', () => { + const {foundation, mockAdapter} = setupTest(); + const opts = ['a', 'SELECTED', 'b']; + const selectedIndex = 1; + td.when(mockAdapter.getNumberOfOptions()).thenReturn(opts.length); + td.when(mockAdapter.getValueForOptionAtIndex(selectedIndex)).thenReturn(opts[selectedIndex]); + td.when(mockAdapter.getTextForOptionAtIndex(selectedIndex)).thenReturn(`${opts[selectedIndex]} text`); + + foundation.setSelectedIndex(selectedIndex); + assert.equal(foundation.getValue(), opts[selectedIndex]); +}); + +test('#getValue() returns an empty string if selected index < 0', () => { + const {foundation} = setupTest(); + assert.equal(foundation.getValue(), ''); +}); diff --git a/test/unit/mdc-select/mdc-select.test.js b/test/unit/mdc-select/mdc-select.test.js index d67948e7aca..59106e67bfb 100644 --- a/test/unit/mdc-select/mdc-select.test.js +++ b/test/unit/mdc-select/mdc-select.test.js @@ -27,6 +27,7 @@ class FakeMenu { bel`
Item 1
`, bel`
Item 2
`, bel`
Item 3
`, + bel`
Item 4 no id
`, ]; this.listen = td.func('menu.listen'); this.unlisten = td.func('menu.unlisten'); @@ -91,6 +92,15 @@ test('#get/setDisabled', () => { assert.isOk(component.disabled); }); +test('#get value', () => { + const {component} = setupTest(); + assert.equal(component.value, ''); + component.selectedIndex = 1; + assert.equal(component.value, 'item-2'); + component.selectedIndex = 3; + assert.equal(component.value, 'Item 4 no id'); +}); + test('#item returns the menu item at the specified index', () => { const {menu, component} = setupTest(); assert.equal(component.item(1), menu.items[1]); @@ -356,3 +366,14 @@ test('adapter#getWindowInnerHeight returns window.innerHeight', () => { const {component} = setupTest(); assert.equal(component.getDefaultFoundation().adapter_.getWindowInnerHeight(), window.innerHeight); }); + +test('adapter#getValueForOptionAtIndex returns the id of the option at the given index', () => { + const {component} = setupTest(); + assert.equal(component.getDefaultFoundation().adapter_.getValueForOptionAtIndex(1), 'item-2'); +}); + +test('adapter#getValueForOptionAtIndex returns the textContent of the option at given index when ' + + 'no id value present', () => { + const {component} = setupTest(); + assert.equal(component.getDefaultFoundation().adapter_.getValueForOptionAtIndex(3), 'Item 4 no id'); +});