Skip to content

Commit

Permalink
feat(select): Add value retrieval mechanisms to JS API
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
traviskaufman committed Feb 27, 2017
1 parent 8cfe07d commit 279ba99
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 5 deletions.
3 changes: 2 additions & 1 deletion demos/select.html
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,8 @@ <h2>Custom Menu + Native Menu on mobile</h2>
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');
Expand Down
12 changes: 10 additions & 2 deletions packages/mdc-select/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}"`);
});
```

Expand Down Expand Up @@ -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. |
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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. |
Expand All @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions packages/mdc-select/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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_;
}
Expand Down
5 changes: 5 additions & 0 deletions packages/mdc-select/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion test/unit/helpers/foundation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]));
}
Expand Down
19 changes: 18 additions & 1 deletion test/unit/mdc-select/foundation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]);
});

Expand Down Expand Up @@ -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(), '');
});
21 changes: 21 additions & 0 deletions test/unit/mdc-select/mdc-select.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class FakeMenu {
bel`<div id="item-1">Item 1</div>`,
bel`<div id="item-2">Item 2</div>`,
bel`<div id="item-3">Item 3</div>`,
bel`<div>Item 4 no id</div>`,
];
this.listen = td.func('menu.listen');
this.unlisten = td.func('menu.unlisten');
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -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');
});

0 comments on commit 279ba99

Please sign in to comment.