Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented allowCreate and more flexible add item on comma functionality #999

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<div>
Expand All @@ -26,6 +27,7 @@ ReactDOM.render(
{/*
<SelectedValuesField label="Option Creation (tags mode)" options={FLAVOURS} allowCreate hint="Enter a value that's NOT in the list, then hit return" />
*/}
<AllowCreate label="Option Creation (tags mode)" allowCreate />
</div>,
document.getElementById('example')
);
64 changes: 64 additions & 0 deletions examples/src/components/AllowCreate.js
Original file line number Diff line number Diff line change
@@ -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 (
<div className="hint">Create options in tag mode</div>
);
},

render () {
return (
<div className="section">
<h3 className="section-heading">{this.props.label}</h3>
<Select
allowCreate={this.props.allowCreate}
value={this.state.value}
multi
placeholder="Select your favourite(s)"
options={this.state.options}
onChange={this.handleSelectChange}
addItemOnKeyCode={188}
/>

{this.renderHint()}
</div>
);
}
});

module.exports = AllowCreate;
25 changes: 13 additions & 12 deletions src/Option.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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 ? (
<div className={className}
onMouseDown={this.blockEvent}
Expand All @@ -79,15 +80,15 @@ const Option = React.createClass({
<div className={className}
style={option.style}
role="option"
onMouseDown={this.handleMouseDown}
onMouseDown={this.handleMouseDown}
onMouseEnter={this.handleMouseEnter}
onMouseMove={this.handleMouseMove}
onTouchStart={this.handleTouchStart}
onTouchMove={this.handleTouchMove}
onTouchEnd={this.handleTouchEnd}
id={instancePrefix + '-option-' + optionIndex}
title={option.title}>
{this.props.children}
{ option.create ? this.props.addLabelText.replace('{label}', option.label) : this.props.children }
</div>
);
}
Expand Down
73 changes: 58 additions & 15 deletions src/Select.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -102,6 +103,7 @@ const Select = React.createClass({
getDefaultProps () {
return {
addLabelText: 'Add "{label}"?',
addItemOnKeyCode: null,
autosize: true,
allowCreate: false,
backspaceRemoves: true,
Expand Down Expand Up @@ -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();
},
Expand Down Expand Up @@ -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);
}
},

Expand Down Expand Up @@ -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();
},

Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand All @@ -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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When using custom keys (valueKey, labelKey) this causes:

Uncaught TypeError: Cannot read property 'toLowerCase' of undefined

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it needs to be changed to:

if (String(option[this.props.labelKey).toLowerCase() === filterValue || String(option[this.props.valueKey]).toLowerCase() === filterValue) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nadavspi cool ! note that you inadvertently forgot a ] 👼

if (String(option[this.props.labelKey]).toLowerCase() === filterValue || String(option[this.props.valueKey]).toLowerCase() === filterValue) {

addNewOption = false;
}
});
if (addNewOption) {
filteredOptions.unshift(this.createNewOption(originalFilterValue));
}
}
return filteredOptions;
},

renderMenu (options, valueArray, focusedOption) {
Expand Down Expand Up @@ -873,6 +915,7 @@ const Select = React.createClass({
onSelect={this.selectValue}
onFocus={this.focusOption}
option={option}
addLabelText={this.props.addLabelText}
isSelected={isSelected}
ref={optionRef}
>
Expand Down
59 changes: 38 additions & 21 deletions test/Select-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' });
};
Expand Down Expand Up @@ -1273,10 +1277,6 @@ describe('Select', () => {
});

describe('with allowCreate=true', () => {

// TODO: allowCreate hasn't been implemented yet in 1.x
return;

beforeEach(() => {

options = [
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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(() => {
Expand Down