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

Creatable and Async components can compose each other #1207

Merged
merged 1 commit into from
Sep 11, 2016
Merged
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -216,9 +216,11 @@ function render (selectProps) {

Property | Type | Description
:---|:---|:---
`children` | function | Child function responsible for creating the inner Select component. This component can be used to compose HOCs (eg Creatable and Async). Expected signature: `(props: Object): PropTypes.element` |
`isOptionUnique` | function | Searches for any matching option within the set of options. This function prevents duplicate options from being created. By default this is a basic, case-sensitive comparison of label and value. Expected signature: `({ option: Object, options: Array, labelKey: string, valueKey: string }): boolean` |
`isValidNewOption` | function | Determines if the current input text represents a valid option. By default any non-empty string will be considered valid. Expected signature: `({ label: string }): boolean` |
`newOptionCreator` | function | Factory to create new option. Expected signature: `({ label: string, labelKey: string, valueKey: string }): Object` |
`promptTextCreator` | function | Creates prompt/placeholder option text. Expected signature: `(filterText: string): string`
`shouldKeyDownEventCreateNewOption` | function | Decides if a keyDown event (eg its `keyCode`) should result in the creation of a new option. ENTER, TAB and comma keys create new options by dfeault. Expected signature: `({ keyCode: number }): boolean` |

### Filtering options
Expand Down
40 changes: 27 additions & 13 deletions src/Async.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const stringOrNode = React.PropTypes.oneOfType([
const Async = React.createClass({
propTypes: {
cache: React.PropTypes.any, // object to use to cache results, can be null to disable cache
children: React.PropTypes.func, // Child function responsible for creating the inner Select component; (props: Object): PropTypes.element
ignoreAccents: React.PropTypes.bool, // whether to strip diacritics when filtering (shared with Select)
ignoreCase: React.PropTypes.bool, // whether to perform case-insensitive filtering (shared with Select)
isLoading: React.PropTypes.bool, // overrides the isLoading state when set to true
Expand All @@ -53,7 +54,7 @@ const Async = React.createClass({
noResultsText: stringOrNode, // placeholder displayed when there are no matching search results (shared with Select)
onInputChange: React.PropTypes.func, // onInputChange handler: function (inputValue) {}
placeholder: stringOrNode, // field placeholder, displayed when there's no value (shared with Select)
searchPromptText: stringOrNode, // label to prompt for search input
searchPromptText: stringOrNode, // label to prompt for search input
searchingText: React.PropTypes.string, // message to display while options are loading
},
getDefaultProps () {
Expand Down Expand Up @@ -141,7 +142,11 @@ const Async = React.createClass({
}) : input;
},
render () {
let { noResultsText } = this.props;
let {
children = defaultChildren,
noResultsText,
...restProps
} = this.props;
let { isLoading, options } = this.state;
if (this.props.isLoading) isLoading = true;
let placeholder = isLoading ? this.props.loadingPlaceholder : this.props.placeholder;
Expand All @@ -150,18 +155,27 @@ const Async = React.createClass({
} else if (!options.length && this._lastInput.length < this.props.minimumInput) {
noResultsText = this.props.searchPromptText;
}
return (
<Select
{...this.props}
ref={(ref) => this.select = ref}
isLoading={isLoading}
noResultsText={noResultsText}
onInputChange={this.loadOptions}
options={options}
placeholder={placeholder}
/>
);

const props = {
...restProps,
isLoading,
noResultsText,
onInputChange: this.loadOptions,
options,
placeholder,
ref: (ref) => {
this.select = ref;
}
};

return children(props);
}
});

function defaultChildren (props) {
return (
<Select {...props} />
);
};

module.exports = Async;
42 changes: 30 additions & 12 deletions src/Creatable.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ const Creatable = React.createClass({
displayName: 'CreatableSelect',

propTypes: {
// Child function responsible for creating the inner Select component
// This component can be used to compose HOCs (eg Creatable and Async)
// (props: Object): PropTypes.element
children: React.PropTypes.func,

// See Select.propTypes.filterOptions
filterOptions: React.PropTypes.any,

Expand Down Expand Up @@ -160,21 +165,34 @@ const Creatable = React.createClass({
},

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}
/>
);
const {
children = defaultChildren,
newOptionCreator,
shouldKeyDownEventCreateNewOption,
...restProps
} = this.props;

const props = {
...restProps,
allowCreate: true,
filterOptions: this.filterOptions,
menuRenderer: this.menuRenderer,
onInputKeyDown: this.onInputKeyDown,
ref: (ref) => {
this.select = ref;
}
};

return children(props);
}
});

function defaultChildren (props) {
return (
<Select {...props} />
);
};

function isOptionUnique ({ option, options, labelKey, valueKey }) {
return options
.filter((existingOption) =>
Expand Down
27 changes: 27 additions & 0 deletions test/Async-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -573,4 +573,31 @@ describe('Async', () => {
expect(loadOptions, 'was called with', 'ware');
});
});

describe('children function', () => {
it('should allow a custom select type to be rendered', () => {
let childProps;
const node = ReactDOM.findDOMNode(
TestUtils.renderIntoDocument(
<Select.Async loadOptions={loadOptions}>
{(props) => {
childProps = props;
return <div>faux select</div>;
}}
</Select.Async>
)
);
expect(node.textContent, 'to equal', 'faux select');
expect(childProps.isLoading, 'to equal', true);
});

it('should render a Select component by default', () => {
const node = ReactDOM.findDOMNode(
TestUtils.renderIntoDocument(
<Select.Async loadOptions={loadOptions} />
)
);
expect(node.className, 'to contain', 'Select');
});
});
});
17 changes: 17 additions & 0 deletions test/Creatable-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,23 @@ describe('Creatable', () => {
expect(options, 'to have length', 1);
});

it('should allow a custom select type to be rendered via the :children property', () => {
let childProps;
createControl({
children: (props) => {
childProps = props;
return <div>faux select</div>;
}
});
expect(creatableNode.textContent, 'to equal', 'faux select');
expect(childProps.allowCreate, 'to equal', true);
});

it('default :children function renders a Select component', () => {
createControl();
expect(creatableNode.className, 'to contain', 'Select');
});

it('default :isOptionUnique function should do a simple equality check for value and label', () => {
const options = [
newOption('foo', 1),
Expand Down