diff --git a/examples/src/app.js b/examples/src/app.js
index bfda0ce816..628f34b60f 100644
--- a/examples/src/app.js
+++ b/examples/src/app.js
@@ -12,6 +12,7 @@ import Multiselect from './components/Multiselect';
import NumericSelect from './components/NumericSelect';
import Virtualized from './components/Virtualized';
import States from './components/States';
+import AllowCreate from './components/AllowCreate';
ReactDOM.render(
@@ -26,6 +27,7 @@ ReactDOM.render(
{/*
*/}
+
,
document.getElementById('example')
);
diff --git a/examples/src/components/AllowCreate.js b/examples/src/components/AllowCreate.js
new file mode 100644
index 0000000000..73d729c619
--- /dev/null
+++ b/examples/src/components/AllowCreate.js
@@ -0,0 +1,64 @@
+import React from 'react';
+import Select from 'react-select';
+
+const FLAVOURS = [
+ { label: 'Chocolate', value: 42 },
+ { label: 'Vanilla', value: 'vanilla' },
+ { label: 'Strawberry', value: 'strawberry' },
+ { label: 'Caramel', value: 'caramel' },
+ { label: 'Cookies and Cream', value: 'cookiescream' },
+ { label: 'Peppermint', value: 'peppermint' },
+];
+
+var AllowCreate = React.createClass({
+ displayName: 'AllowCreate',
+
+ propTypes: {
+ allowCreate: React.PropTypes.bool,
+ label: React.PropTypes.string,
+ },
+
+ getInitialState () {
+ return {
+ disabled: false,
+ crazy: false,
+ options: FLAVOURS,
+ value: [],
+ };
+ },
+
+ onLabelClick (data, event) {
+ console.log(data, event);
+ },
+
+ handleSelectChange (value){
+ this.setState({ value });
+ },
+
+ renderHint () {
+ return (
+ Create options in tag mode
+ );
+ },
+
+ render () {
+ return (
+
+
{this.props.label}
+
+
+ {this.renderHint()}
+
+ );
+ }
+});
+
+module.exports = AllowCreate;
diff --git a/src/Option.js b/src/Option.js
index 3ab7b481c2..1ea0ca2079 100644
--- a/src/Option.js
+++ b/src/Option.js
@@ -3,18 +3,20 @@ import classNames from 'classnames';
const Option = React.createClass({
propTypes: {
+ addLabelText: React.PropTypes.string, // text to display with value while creating new option
children: React.PropTypes.node,
- className: React.PropTypes.string, // className (based on mouse position)
+ className: React.PropTypes.string, // className (based on mouse position)
instancePrefix: React.PropTypes.string.isRequired, // unique prefix for the ids (used for aria)
- isDisabled: React.PropTypes.bool, // the option is disabled
- isFocused: React.PropTypes.bool, // the option is focused
- isSelected: React.PropTypes.bool, // the option is selected
- onFocus: React.PropTypes.func, // method to handle mouseEnter on option element
- onSelect: React.PropTypes.func, // method to handle click on option element
- onUnfocus: React.PropTypes.func, // method to handle mouseLeave on option element
- option: React.PropTypes.object.isRequired, // object that is base for that option
- optionIndex: React.PropTypes.number, // index of the option, used to generate unique ids for aria
+ isDisabled: React.PropTypes.bool, // the option is disabled
+ isFocused: React.PropTypes.bool, // the option is focused
+ isSelected: React.PropTypes.bool, // the option is selected
+ onFocus: React.PropTypes.func, // method to handle mouseEnter on option element
+ onSelect: React.PropTypes.func, // method to handle click on option element
+ onUnfocus: React.PropTypes.func, // method to handle mouseLeave on option element
+ option: React.PropTypes.object.isRequired, // object that is base for that option
+ optionIndex: React.PropTypes.number // index of the option, used to generate unique ids for aria
},
+
blockEvent (event) {
event.preventDefault();
event.stopPropagation();
@@ -68,7 +70,6 @@ const Option = React.createClass({
render () {
var { option, instancePrefix, optionIndex } = this.props;
var className = classNames(this.props.className, option.className);
-
return option.disabled ? (
- {this.props.children}
+ { option.create ? this.props.addLabelText.replace('{label}', option.label) : this.props.children }
);
}
diff --git a/src/Select.js b/src/Select.js
index fd58056552..3b198b827c 100644
--- a/src/Select.js
+++ b/src/Select.js
@@ -29,6 +29,7 @@ const Select = React.createClass({
displayName: 'Select',
propTypes: {
+ addItemOnKeyCode: React.PropTypes.number, // The key code number that should trigger adding a tag if multi and allowCreate are enabled
addLabelText: React.PropTypes.string, // placeholder displayed when you want to add a label on a multi-value input
allowCreate: React.PropTypes.bool, // whether to allow creation of new entries
'aria-label': React.PropTypes.string, // Aria label (for assistive tech)
@@ -102,6 +103,7 @@ const Select = React.createClass({
getDefaultProps () {
return {
addLabelText: 'Add "{label}"?',
+ addItemOnKeyCode: null,
autosize: true,
allowCreate: false,
backspaceRemoves: true,
@@ -437,17 +439,15 @@ const Select = React.createClass({
break;
case 36: // home key
this.focusStartOption();
+ default:
+ if (this.props.allowCreate && this.props.multi && event.keyCode === this.props.addItemOnKeyCode) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.selectFocusedOption();
+ } else {
+ return;
+ }
break;
- // case 188: // ,
- // if (this.props.allowCreate && this.props.multi) {
- // event.preventDefault();
- // event.stopPropagation();
- // this.selectFocusedOption();
- // } else {
- // return;
- // }
- // break;
- default: return;
}
event.preventDefault();
},
@@ -492,7 +492,13 @@ const Select = React.createClass({
let { options, valueKey } = this.props;
if (!options) return;
for (var i = 0; i < options.length; i++) {
- if (options[i][valueKey] === value) return options[i];
+ if (options[i][valueKey] === value) {
+ return options[i];
+ }
+ }
+
+ if (this.props.allowCreate && value !== '') {
+ return this.createNewOption(value);
}
},
@@ -543,7 +549,13 @@ const Select = React.createClass({
removeValue (value) {
var valueArray = this.getValueArray(this.props.value);
- this.setValue(valueArray.filter(i => i !== value));
+ this.setValue(valueArray.filter(i => {
+ if (i.create) {
+ return i[this.props.valueKey] !== value[this.props.valueKey] && i[this.props.labelKey] !== value[this.props.labelKey];
+ } else {
+ return i !== value;
+ }
+ }));
this.focus();
},
@@ -660,6 +672,21 @@ const Select = React.createClass({
}
},
+ createNewOption (value) {
+ let newOption = {};
+
+ if (this.props.newOptionCreator) {
+ newOption = this.props.newOptionCreator(value);
+ } else {
+ newOption[this.props.valueKey] = value;
+ newOption[this.props.labelKey] = value;
+ }
+
+ newOption.create = true;
+
+ return newOption;
+ },
+
renderLoading () {
if (!this.props.isLoading) return;
return (
@@ -798,9 +825,11 @@ const Select = React.createClass({
filterOptions (excludeOptions) {
var filterValue = this.state.inputValue;
+ var originalFilterValue = filterValue;
var options = this.props.options || [];
+ var filteredOptions = [];
if (typeof this.props.filterOptions === 'function') {
- return this.props.filterOptions.call(this, options, filterValue, excludeOptions);
+ filteredOptions = this.props.filterOptions.call(this, options, filterValue, excludeOptions);
} else if (this.props.filterOptions) {
if (this.props.ignoreAccents) {
filterValue = stripDiacritics(filterValue);
@@ -809,7 +838,7 @@ const Select = React.createClass({
filterValue = filterValue.toLowerCase();
}
if (excludeOptions) excludeOptions = excludeOptions.map(i => i[this.props.valueKey]);
- return options.filter(option => {
+ filteredOptions = options.filter(option => {
if (excludeOptions && excludeOptions.indexOf(option[this.props.valueKey]) > -1) return false;
if (this.props.filterOption) return this.props.filterOption.call(this, option, filterValue);
if (!filterValue) return true;
@@ -832,8 +861,21 @@ const Select = React.createClass({
);
});
} else {
- return options;
+ filteredOptions = options;
+ }
+ if (this.props.allowCreate && filterValue) {
+ let addNewOption = true;
+ //NOTE: only add the "Add" option if none of the options are an exact match
+ filteredOptions.map(option => {
+ if (String(option.label).toLowerCase() === filterValue || String(option.value).toLowerCase() === filterValue) {
+ addNewOption = false;
+ }
+ });
+ if (addNewOption) {
+ filteredOptions.unshift(this.createNewOption(originalFilterValue));
+ }
}
+ return filteredOptions;
},
renderMenu (options, valueArray, focusedOption) {
@@ -873,6 +915,7 @@ const Select = React.createClass({
onSelect={this.selectValue}
onFocus={this.focusOption}
option={option}
+ addLabelText={this.props.addLabelText}
isSelected={isSelected}
ref={optionRef}
>
diff --git a/test/Select-test.js b/test/Select-test.js
index aa96fd8511..8430ab1b51 100644
--- a/test/Select-test.js
+++ b/test/Select-test.js
@@ -68,6 +68,10 @@ describe('Select', () => {
TestUtils.Simulate.keyDown(searchInputNode, { keyCode: 65, key: 'a' });
};
+ var pressCommaToAccept = () =>{
+ TestUtils.Simulate.keyDown(searchInputNode, { keyCode: 188, key: 'Comma' });
+ };
+
var pressEnterToAccept = () => {
TestUtils.Simulate.keyDown(searchInputNode, { keyCode: 13, key: 'Enter' });
};
@@ -1273,10 +1277,6 @@ describe('Select', () => {
});
describe('with allowCreate=true', () => {
-
- // TODO: allowCreate hasn't been implemented yet in 1.x
- return;
-
beforeEach(() => {
options = [
@@ -1309,17 +1309,17 @@ describe('Select', () => {
it('fires an onChange with the new value when selecting the Add option', () => {
typeSearchText('xyz');
- TestUtils.Simulate.click(ReactDOM.findDOMNode(instance).querySelector('.Select-menu .Select-option'));
+ TestUtils.Simulate.mouseDown(ReactDOM.findDOMNode(instance).querySelector('.Select-menu .Select-option'));
- expect(onChange, 'was called with', 'xyz');
+ expect(onChange, 'was called with', { value: 'xyz', label: 'xyz', create: true });
});
it('allows updating the options with a new label, following the onChange', () => {
typeSearchText('xyz');
- TestUtils.Simulate.click(ReactDOM.findDOMNode(instance).querySelector('.Select-menu .Select-option'));
+ TestUtils.Simulate.mouseDown(ReactDOM.findDOMNode(instance).querySelector('.Select-menu .Select-option'));
- expect(onChange, 'was called with', 'xyz');
+ expect(onChange, 'was called with', { value: 'xyz', label: 'xyz', create: true });
// Now the client adds the option, with a new label
wrapper.setPropsForChild({
@@ -1358,32 +1358,49 @@ describe('Select', () => {
'to have text', 'Add test to values?');
});
- it('does not display the option label when an existing value is entered', () => {
+ it('does not display the add option label when an existing value is entered', () => {
typeSearchText('zzzzz');
expect(ReactDOM.findDOMNode(instance).querySelectorAll('.Select-menu .Select-option'),
'to have length', 1);
expect(ReactDOM.findDOMNode(instance), 'queried for first', '.Select-menu .Select-option',
- 'to have text', 'Add zzzzz to values?');
+ 'to have text', 'test value');
});
+ });
+
+ describe('with addItemOnKeyCode=188', () => {
+ beforeEach(() => {
- it('renders the existing option and an add option when an existing display label is entered', () => {
+ options = [
+ { value: 'one', label: 'One' },
+ { value: 'two', label: 'Two' },
+ { value: 'got spaces', label: 'Label for spaces' },
+ { value: 'gotnospaces', label: 'Label for gotnospaces' },
+ { value: 'abc 123', label: 'Label for abc 123' },
+ { value: 'three', label: 'Three' },
+ { value: 'zzzzz', label: 'test value' }
+ ];
- typeSearchText('test value');
+ // Render an instance of the component
+ wrapper = createControlWithWrapper({
+ value: [],
+ options: options,
+ allowCreate: true,
+ multi: true,
+ searchable: true,
+ addLabelText: 'Add {label} to values?',
+ addItemOnKeyCode: 188
+ });
+ });
- // First item should be the add option (as the "value" is not in the collection)
- expect(ReactDOM.findDOMNode(instance).querySelectorAll('.Select-menu .Select-option')[0],
- 'to have text', 'Add test value to values?');
- // Second item should be the existing option with the matching label
- expect(ReactDOM.findDOMNode(instance).querySelectorAll('.Select-menu .Select-option')[1],
- 'to have text', 'test value');
- expect(ReactDOM.findDOMNode(instance).querySelectorAll('.Select-menu .Select-option'),
- 'to have length', 2);
+ it('has an "Add xyz" option when entering xyz', () => {
+ typeSearchText('xyz');
+ pressCommaToAccept();
+ expect(onChange, 'was called with', [{ value: 'xyz', label: 'xyz', create: true }]);
});
});
-
describe('with multi-select', () => {
beforeEach(() => {