-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement allowCreate and newOptionCreator
Added new Creatable HOC that wraps Select and adds support for creating new options. This HOC provides reasonable default behavior (eg create a new option on ENTER/TAB/comma) but also allows users to override defaults via props. No create-option-specific logic is inside of the base Select component and the logic within Creatable is fully customizable. Added new onInputKeyDown prop to Select (a mirror to onInputChange) that allows users to tap into key-down events and prevent default Select handling. (This is key to how new options are created.) Pulled default filterOptions and menuRenderer props out of Select and into separate modules. I think this lines up with @JedWatson's long-term goals. It was also necessary to enable better sharing between the base Select and the new Creatable HOC.
- Loading branch information
Showing
8 changed files
with
636 additions
and
103 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import React from 'react'; | ||
import Select from 'react-select'; | ||
|
||
var CreatableDemo = React.createClass({ | ||
displayName: 'CreatableDemo', | ||
propTypes: { | ||
hint: React.PropTypes.string, | ||
label: React.PropTypes.string | ||
}, | ||
getInitialState () { | ||
return { | ||
multi: true, | ||
multiValue: [], | ||
options: [ | ||
{ value: 'R', label: 'Red' }, | ||
{ value: 'G', label: 'Green' }, | ||
{ value: 'B', label: 'Blue' } | ||
], | ||
value: undefined | ||
}; | ||
}, | ||
handleOnChange (value) { | ||
const { multi } = this.state; | ||
if (multi) { | ||
this.setState({ multiValue: value }); | ||
} else { | ||
this.setState({ value }); | ||
} | ||
}, | ||
render () { | ||
const { multi, multiValue, options, value } = this.state; | ||
return ( | ||
<div className="section"> | ||
<h3 className="section-heading">{this.props.label}</h3> | ||
<Select.Creatable | ||
multi={multi} | ||
options={options} | ||
onChange={this.handleOnChange} | ||
value={multi ? multiValue : value} | ||
/> | ||
<div className="hint">{this.props.hint}</div> | ||
<div className="checkbox-list"> | ||
<label className="checkbox"> | ||
<input | ||
type="radio" | ||
className="checkbox-control" | ||
checked={multi} | ||
onChange={() => this.setState({ multi: true })} | ||
/> | ||
<span className="checkbox-label">Multiselect</span> | ||
</label> | ||
<label className="checkbox"> | ||
<input | ||
type="radio" | ||
className="checkbox-control" | ||
checked={!multi} | ||
onChange={() => this.setState({ multi: false })} | ||
/> | ||
<span className="checkbox-label">Single Value</span> | ||
</label> | ||
</div> | ||
</div> | ||
); | ||
} | ||
}); | ||
|
||
module.exports = CreatableDemo; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,212 @@ | ||
import React from 'react'; | ||
import Select from './Select'; | ||
import defaultFilterOptions from './utils/defaultFilterOptions'; | ||
import defaultMenuRenderer from './utils/defaultMenuRenderer'; | ||
|
||
const Creatable = React.createClass({ | ||
displayName: 'CreatableSelect', | ||
|
||
propTypes: { | ||
// See Select.propTypes.filterOptions | ||
filterOptions: React.PropTypes.any, | ||
|
||
// Searches for any matching option within the set of options. | ||
// This function prevents duplicate options from being created. | ||
// (newOption: Object, options: Array, labelKey: string, valueKey: string): boolean | ||
isOptionUnique: React.PropTypes.func, | ||
|
||
// Determines if the current input text represents a valid option. | ||
// (label: string): boolean | ||
isValidNewOption: React.PropTypes.func, | ||
|
||
// See Select.propTypes.menuRenderer | ||
menuRenderer: React.PropTypes.any, | ||
|
||
// Factory to create new option. | ||
// (label: string, labelKey: string, valueKey: string): Object | ||
newOptionCreator: React.PropTypes.func, | ||
|
||
// Creates prompt/placeholder option text. | ||
// (filterText: string): string | ||
promptTextCreator: React.PropTypes.func, | ||
|
||
// Decides if a keyDown event (eg its `keyCode`) should result in the creation of a new option. | ||
shouldKeyDownEventCreateNewOption: React.PropTypes.func, | ||
}, | ||
|
||
// Default prop methods | ||
statics: { | ||
isOptionUnique, | ||
isValidNewOption, | ||
newOptionCreator, | ||
promptTextCreator, | ||
shouldKeyDownEventCreateNewOption | ||
}, | ||
|
||
getDefaultProps () { | ||
return { | ||
filterOptions: defaultFilterOptions, | ||
isOptionUnique, | ||
isValidNewOption, | ||
menuRenderer: defaultMenuRenderer, | ||
newOptionCreator, | ||
promptTextCreator, | ||
shouldKeyDownEventCreateNewOption, | ||
}; | ||
}, | ||
|
||
createNewOption () { | ||
const { isValidNewOption, newOptionCreator, shouldKeyDownEventCreateNewOption } = this.props; | ||
const { labelKey, options, valueKey } = this.select.props; | ||
|
||
const inputValue = this.select.getInputValue(); | ||
|
||
if (isValidNewOption(inputValue)) { | ||
const newOption = newOptionCreator(inputValue, labelKey, valueKey); | ||
const isOptionUnique = this.isOptionUnique({ newOption }); | ||
|
||
// Don't add the same option twice. | ||
if (isOptionUnique) { | ||
options.unshift(newOption); | ||
|
||
this.select.selectValue(newOption); | ||
} | ||
} | ||
}, | ||
|
||
filterOptions (...params) { | ||
const { filterOptions, isValidNewOption, promptTextCreator } = this.props; | ||
|
||
const filteredOptions = filterOptions(...params); | ||
|
||
const inputValue = this.select | ||
? this.select.getInputValue() | ||
: ''; | ||
|
||
if (isValidNewOption(inputValue)) { | ||
const { newOptionCreator } = this.props; | ||
const { labelKey, options, valueKey } = this.select.props; | ||
|
||
const newOption = newOptionCreator(inputValue, labelKey, valueKey); | ||
|
||
// TRICKY Compare to all options (not just filtered options) in case option has already been selected). | ||
// For multi-selects, this would remove it from the filtered list. | ||
const isOptionUnique = this.isOptionUnique({ | ||
newOption, | ||
options | ||
}); | ||
|
||
if (isOptionUnique) { | ||
const prompt = promptTextCreator(inputValue); | ||
|
||
this._createPlaceholderOption = newOptionCreator(prompt, labelKey, valueKey); | ||
|
||
filteredOptions.unshift(this._createPlaceholderOption); | ||
} | ||
} | ||
|
||
return filteredOptions; | ||
}, | ||
|
||
isOptionUnique ({ | ||
newOption, | ||
options | ||
}) { | ||
if (!this.select) { | ||
return false; | ||
} | ||
|
||
const { isOptionUnique } = this.props; | ||
const { labelKey, valueKey } = this.select.props; | ||
|
||
options = options || this.select.filterOptions(); | ||
|
||
return isOptionUnique(newOption, options, labelKey, valueKey); | ||
}, | ||
|
||
menuRenderer (params) { | ||
const { menuRenderer } = this.props; | ||
|
||
return menuRenderer({ | ||
...params, | ||
onSelect: this.onOptionSelect | ||
}); | ||
}, | ||
|
||
onInputKeyDown (event) { | ||
const { shouldKeyDownEventCreateNewOption } = this.props; | ||
const focusedOption = this.select.getFocusedOption(); | ||
|
||
if ( | ||
focusedOption && | ||
focusedOption === this._createPlaceholderOption && | ||
shouldKeyDownEventCreateNewOption(event.keyCode) | ||
) { | ||
this.createNewOption(); | ||
|
||
// Prevent decorated Select from doing anything additional with this keyDown event | ||
event.preventDefault(); | ||
} | ||
}, | ||
|
||
onOptionSelect (option, event) { | ||
if (option === this._createPlaceholderOption) { | ||
this.createNewOption(); | ||
} else { | ||
this.select.selectValue(option); | ||
} | ||
}, | ||
|
||
render () { | ||
const { newOptionCreator, shouldKeyDownEventCreateNewOption, ...restProps } = this.props; | ||
|
||
return ( | ||
<Select | ||
{...restProps} | ||
allowCreate | ||
filterOptions={this.filterOptions} | ||
menuRenderer={this.menuRenderer} | ||
onInputKeyDown={this.onInputKeyDown} | ||
ref={(ref) => this.select = ref} | ||
/> | ||
); | ||
} | ||
}); | ||
|
||
function isOptionUnique (newOption, options, labelKey, valueKey) { | ||
return options | ||
.filter((option) => | ||
option[labelKey] === newOption[labelKey] || | ||
option[valueKey] === newOption[valueKey] | ||
) | ||
.length === 0; | ||
}; | ||
|
||
function isValidNewOption (label) { | ||
return !!label; | ||
}; | ||
|
||
function newOptionCreator (label, labelKey, valueKey) { | ||
const option = {}; | ||
option[valueKey] = label; | ||
option[labelKey] = label; | ||
option.className = 'Select-create-option-placeholder'; | ||
return option; | ||
}; | ||
|
||
function promptTextCreator (label) { | ||
return `Create option "${label}"`; | ||
} | ||
|
||
function shouldKeyDownEventCreateNewOption (keyCode, label) { | ||
switch (keyCode) { | ||
case 9: // TAB | ||
case 13: // ENTER | ||
case 188: // COMMA | ||
return true; | ||
} | ||
|
||
return false; | ||
}; | ||
|
||
module.exports = Creatable; |
Oops, something went wrong.
7fe723f
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bvaughn This is awesome! Can you use this with
async
? What's the best way to pull the options from a REST api?7fe723f
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not yet! Hope to add support for that combo soon. Traveling in China this week and my time is kind of limited. :)
7fe723f
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PS Thanks 😁
7fe723f
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@bvaughn Awesome work dude. And no hurry on it, enjoy your trip!