diff --git a/examples/src/app.js b/examples/src/app.js index a13bc93997..d8c83d7ce2 100644 --- a/examples/src/app.js +++ b/examples/src/app.js @@ -11,6 +11,7 @@ import CustomComponents from './components/CustomComponents'; import CustomRender from './components/CustomRender'; import Multiselect from './components/Multiselect'; import NumericSelect from './components/NumericSelect'; +import BooleanSelect from './components/BooleanSelect'; import Virtualized from './components/Virtualized'; import States from './components/States'; @@ -22,6 +23,7 @@ ReactDOM.render( + +

{this.props.label}

+ + Multi-Select + + + + + +
This example uses simple boolean values
+ + ); + } +}); + +module.exports = ValuesAsBooleansField; diff --git a/src/Select.js b/src/Select.js index 3f45b5a618..46aa8263e9 100644 --- a/src/Select.js +++ b/src/Select.js @@ -20,11 +20,12 @@ import Option from './Option'; import Value from './Value'; function stringifyValue (value) { - if (typeof value === 'string') { + const valueType = typeof value; + if (valueType === 'string') { return value; - } else if (typeof value === 'object') { + } else if (valueType === 'object') { return JSON.stringify(value); - } else if (value || value === 0) { + } else if (valueType === 'number' || valueType === 'boolean') { return String(value); } else { return ''; @@ -561,7 +562,8 @@ const Select = React.createClass({ * @param {Object} props - the Select component's props (or nextProps) */ expandValue (value, props) { - if (typeof value !== 'string' && typeof value !== 'number') return value; + const valueType = typeof value; + if (valueType !== 'string' && valueType !== 'number' && valueType !== 'boolean') return value; let { options, valueKey } = props; if (!options) return; for (var i = 0; i < options.length; i++) { diff --git a/test/Select-test.js b/test/Select-test.js index 4e26660a7b..21866b2c17 100644 --- a/test/Select-test.js +++ b/test/Select-test.js @@ -910,6 +910,263 @@ describe('Select', () => { }); }); + describe('with values as booleans', () => { + beforeEach(() => { + options = [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' }, + ]; + + wrapper = createControlWithWrapper({ + value: true, + name: 'field', + options: options, + simpleValue: true, + }); + }); + + it('selects the initial value', () => { + expect(ReactDOM.findDOMNode(instance), 'queried for first', DISPLAYED_SELECTION_SELECTOR, + 'to have text', 'Yes'); + }); + + it('set the initial value of the hidden input control', () => { + expect(ReactDOM.findDOMNode(wrapper).querySelector(FORM_VALUE_SELECTOR).value, 'to equal', 'true' ); + }); + + it('updates the value when the value prop is set', () => { + wrapper.setPropsForChild({ value: false }); + expect(ReactDOM.findDOMNode(instance), 'queried for first', DISPLAYED_SELECTION_SELECTOR, + 'to have text', 'No'); + }); + + it('updates the value of the hidden input control after new value prop', () => { + wrapper.setPropsForChild({ value: false }); + expect(ReactDOM.findDOMNode(wrapper).querySelector(FORM_VALUE_SELECTOR).value, 'to equal', 'false' ); + }); + + it('calls onChange with the new value as a boolean', () => { + clickArrowToOpen(); + pressDown(); + pressEnterToAccept(); + expect(onChange, 'was called with', false); + }); + + it('supports setting the value via prop', () => { + wrapper.setPropsForChild({ value: false }); + expect(ReactDOM.findDOMNode(instance), 'queried for first', DISPLAYED_SELECTION_SELECTOR, + 'to have text', 'No'); + }); + + describe('with multi=true', () => { + + beforeEach(() => { + + options = [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' } + ]; + + wrapper = createControlWithWrapper({ + value: [true, false], + options: options, + multi: true, + searchable: true + }); + }); + + it('selects the initial value', () => { + + expect(instance, 'to contain', + +
Yes
+
No
+
); + }); + + it('calls onChange with the correct value when true option is deselected', () => { + + var removeIcons = ReactDOM.findDOMNode(instance).querySelectorAll('.Select-value .Select-value-icon'); + TestUtils.Simulate.mouseDown(removeIcons[0]); + expect(onChange, 'was called with', [{ value: false, label: 'No' }]); + }); + + it('supports updating the values via props', () => { + + wrapper.setPropsForChild({ + value: [false] + }); + + expect(instance, 'to contain', + +
No
+
); + }); + + it('supports updating the value to a single value', () => { + + wrapper.setPropsForChild({ + value: true + }); + + expect(instance, 'to contain', + +
Yes
+
); + }); + + it('supports updating the value to single value of false', () => { + + // This test is specifically in case there's a "if (value) {... " somewhere + wrapper.setPropsForChild({ + value: false + }); + + expect(instance, 'to contain', + +
No
+
); + }); + + it('calls onChange with the correct values when multiple options are selected', () => { + wrapper.setPropsForChild({ + value: [true] + }); + + typeSearchText('No'); + pressEnterToAccept(); // Select 'No' + + expect(onChange, 'was called with', [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' } + ]); + }); + }); + + describe('searching', () => { + + let searchOptions = [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' } + ]; + + describe('with matchPos=any and matchProp=any', () => { + beforeEach(() => { + instance = createControl({ + matchPos: 'any', + matchProp: 'any', + options: searchOptions + }); + }); + + it('finds text anywhere in value', () => { + + typeSearchText('fal'); + expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option', + 'to satisfy', [ + expect.it('to have text', 'No'), + ]); + }); + + it('finds text at end', () => { + + typeSearchText('se'); + expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option', + 'to satisfy', [ + expect.it('to have text', 'No'), + ]); + }); + }); + + describe('with matchPos=start and matchProp=any', () => { + + beforeEach(() => { + instance = createControl({ + matchPos: 'start', + matchProp: 'any', + options: searchOptions + }); + }); + + it('finds text at the start of the value', () => { + + typeSearchText('fa'); + expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option', + 'to satisfy', [ + expect.it('to have text', 'No') + ]); + }); + + it('does not match text at end', () => { + + typeSearchText('se'); + expect(ReactDOM.findDOMNode(instance), 'to contain elements matching', + '.Select-noresults'); + expect(ReactDOM.findDOMNode(instance), 'to contain no elements matching', + '.Select-option'); + }); + }); + + describe('with matchPos=any and matchProp=value', () => { + beforeEach(() => { + instance = createControl({ + matchPos: 'any', + matchProp: 'value', + options: searchOptions + }); + }); + + it('finds text anywhere in value', () => { + + typeSearchText('al'); + expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option', + 'to satisfy', [ + expect.it('to have text', 'No'), + ]); + }); + + it('finds text at end', () => { + + typeSearchText('e'); + expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option', + 'to satisfy', [ + expect.it('to have text', 'Yes'), + expect.it('to have text', 'No') + ]); + }); + }); + + describe('with matchPos=start and matchProp=value', () => { + + beforeEach(() => { + instance = createControl({ + matchPos: 'start', + matchProp: 'value', + options: searchOptions + }); + }); + + it('finds text at the start of the value', () => { + + typeSearchText('tr'); + expect(ReactDOM.findDOMNode(instance), 'queried for', '.Select-option', + 'to satisfy', [ + expect.it('to have text', 'Yes') + ]); + }); + + it('does not match text at end', () => { + + typeSearchText('e'); + expect(ReactDOM.findDOMNode(instance), 'to contain elements matching', + '.Select-noresults'); + expect(ReactDOM.findDOMNode(instance), 'to contain no elements matching', + '.Select-option'); + }); + }); + }); + }); + describe('with options and value', () => { beforeEach(() => {