Skip to content

Commit

Permalink
Refactored Async. Cache not yet implemented. Pushing for discussion
Browse files Browse the repository at this point in the history
  • Loading branch information
bvaughn committed Sep 18, 2016
1 parent 740bf26 commit ed8d087
Show file tree
Hide file tree
Showing 6 changed files with 309 additions and 735 deletions.
1 change: 0 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ Multiple values are now submitted in multiple form fields, which results in an a
## New Select.Async Component

`loadingPlaceholder` prop
`autoload` changed to `minimumInput` and now controls the minimum input to load options
`cacheAsyncResults` -> `cache` (new external cache support) - defaults to true

## Fixes & Other Changes
Expand Down
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,6 @@ function onInputKeyDown(event) {
placeholder | string\|node | 'Select ...' | field placeholder, displayed when there's no value
scrollMenuIntoView | bool | true | whether the viewport will shift to display the entire menu when engaged
searchable | bool | true | whether to enable searching feature or not
searchingText | string | 'Searching...' | message to display whilst options are loading via asyncOptions, or when `isLoading` is true
searchPromptText | string\|node | 'Type to search' | label to prompt for search input
tabSelectsValue | bool | true | whether to select the currently focused value when the `[tab]` key is pressed
value | any | undefined | initial field value
Expand Down
6 changes: 5 additions & 1 deletion examples/src/components/GithubUsers.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ const GithubUsers = React.createClass({
});
},
getUsers (input) {
if (!input) {
return Promise.resolve({ options: [] });
}

return fetch(`https://api.github.com/search/users?q=${input}`)
.then((response) => response.json())
.then((json) => {
Expand All @@ -53,7 +57,7 @@ const GithubUsers = React.createClass({
return (
<div className="section">
<h3 className="section-heading">{this.props.label}</h3>
<AsyncComponent multi={this.state.multi} value={this.state.value} onChange={this.onChange} onValueClick={this.gotoUser} valueKey="id" labelKey="login" loadOptions={this.getUsers} minimumInput={1} backspaceRemoves={false} />
<AsyncComponent multi={this.state.multi} value={this.state.value} onChange={this.onChange} onValueClick={this.gotoUser} valueKey="id" labelKey="login" loadOptions={this.getUsers} backspaceRemoves={false} />
<div className="checkbox-list">
<label className="checkbox">
<input type="radio" className="checkbox-control" checked={this.state.multi} onChange={this.switchToMulti}/>
Expand Down
264 changes: 114 additions & 150 deletions src/Async.js
Original file line number Diff line number Diff line change
@@ -1,181 +1,145 @@
import React from 'react';

import React, { Component, PropTypes } from 'react';
import Select from './Select';
import stripDiacritics from './utils/stripDiacritics';

let requestId = 0;
// @TODO Implement cache

const propTypes = {
autoload: React.PropTypes.bool.isRequired,
children: React.PropTypes.func.isRequired, // 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)
loadingPlaceholder: PropTypes.string.isRequired,
loadOptions: React.PropTypes.func.isRequired,
options: PropTypes.array.isRequired,
placeholder: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.node
]),
searchPromptText: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.node
]),
};

function initCache (cache) {
if (cache && typeof cache !== 'object') {
cache = {};
const defaultProps = {
autoload: true,
children: defaultChildren,
ignoreAccents: true,
ignoreCase: true,
loadingPlaceholder: 'Loading...',
options: [],
searchPromptText: 'Type to search',
};

export default class Async extends Component {
constructor (props, context) {
super(props, context);

this.state = {
isLoading: false,
options: props.options,
};

this._onInputChange = this._onInputChange.bind(this);
}
return cache ? cache : null;
}

function updateCache (cache, input, data) {
if (!cache) return;
cache[input] = data;
}
componentDidMount () {
const { autoload } = this.props;

function getFromCache (cache, input) {
if (!cache) return;
for (let i = input.length; i >= 0; --i) {
let cacheKey = input.slice(0, i);
if (cache[cacheKey] && (input === cacheKey || cache[cacheKey].complete)) {
return cache[cacheKey];
if (autoload) {
this.loadOptions('');
}
}
}

function thenPromise (promise, callback) {
if (!promise || typeof promise.then !== 'function') return;
return promise.then((data) => {
callback(null, data);
}, (err) => {
callback(err);
});
}
componentWillUpdate (nextProps, nextState) {
const propertiesToSync = ['options'];
propertiesToSync.forEach((prop) => {
if (this.props[prop] !== nextProps[prop]) {
this.setState({
[prop]: nextProps[prop]
});
}
});
}

const stringOrNode = React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.node
]);

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
loadOptions: React.PropTypes.func.isRequired, // function to call to load options asynchronously
loadingPlaceholder: React.PropTypes.string, // replaces the placeholder while options are loading
minimumInput: React.PropTypes.number, // the minimum number of characters that trigger loadOptions
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
searchingText: React.PropTypes.string, // message to display while options are loading
},
getDefaultProps () {
return {
cache: true,
ignoreAccents: true,
ignoreCase: true,
loadingPlaceholder: 'Loading...',
minimumInput: 0,
searchingText: 'Searching...',
searchPromptText: 'Type to search',
};
},
getInitialState () {
return {
cache: initCache(this.props.cache),
isLoading: false,
options: [],
loadOptions (inputValue) {
const { loadOptions } = this.props;

const callback = (error, data) => {
if (callback === this._callback) {
this._callback = null;

const options = data && data.options || [];

this.setState({
isLoading: false,
options
});
}
};
},
componentWillMount () {
this._lastInput = '';
},
componentDidMount () {
this.loadOptions('');
},
componentWillReceiveProps (nextProps) {
if (nextProps.cache !== this.props.cache) {
this.setState({
cache: initCache(nextProps.cache),
});

// Ignore all but the most recent request
this._callback = callback;

const promise = loadOptions(inputValue, callback);
if (promise) {
promise.then(
(data) => callback(null, data),
(error) => callback(error)
);
}
},
focus () {
this.select.focus();
},

This comment has been minimized.

Copy link
@mcls

mcls Oct 27, 2016

Contributor

Was focus removed accidentally here? Or is there another reason?

resetState () {
this._currentRequestId = -1;
this.setState({
isLoading: false,
options: [],
});
},
getResponseHandler (input) {
let _requestId = this._currentRequestId = requestId++;
return (err, data) => {
if (err) throw err;
if (!this.isMounted()) return;
updateCache(this.state.cache, input, data);
if (_requestId !== this._currentRequestId) return;

if (
this._callback &&
!this.state.isLoading
) {
this.setState({
isLoading: false,
options: data && data.options || [],
isLoading: true
});
};
},
loadOptions (input) {
if (this.props.onInputChange) {
let nextState = this.props.onInputChange(input);
// Note: != used deliberately here to catch undefined and null
if (nextState != null) {
input = '' + nextState;
}
}
if (this.props.ignoreAccents) input = stripDiacritics(input);
if (this.props.ignoreCase) input = input.toLowerCase();

this._lastInput = input;
if (input.length < this.props.minimumInput) {
return this.resetState();
return inputValue;
}

_onInputChange (inputValue) {
const { ignoreAccents, ignoreCase } = this.props;

if (ignoreAccents) {
inputValue = stripDiacritics(inputValue);
}
let cacheResult = getFromCache(this.state.cache, input);
if (cacheResult && Array.isArray(cacheResult.options)) {
return this.setState({
options: cacheResult.options,
});

if (ignoreCase) {
inputValue = inputValue.toLowerCase();
}
this.setState({
isLoading: true,
});
let responseHandler = this.getResponseHandler(input);
let inputPromise = thenPromise(this.props.loadOptions(input, responseHandler), responseHandler);
return inputPromise ? inputPromise.then(() => {
return input;
}) : input;
},

return this.loadOptions(inputValue);
}

render () {
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;
if (isLoading) {
noResultsText = this.props.searchingText;
} else if (!options.length && this._lastInput.length < this.props.minimumInput) {
noResultsText = this.props.searchPromptText;
}
const { children, loadingPlaceholder, placeholder, searchPromptText } = this.props;
const { isLoading, options } = this.state;

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

return children(props);
return children({
...this.props,
...props,
isLoading,
onInputChange: this._onInputChange
});
}
});
}

Async.propTypes = propTypes;
Async.defaultProps = defaultProps;

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

module.exports = Async;
4 changes: 0 additions & 4 deletions src/AsyncCreatable.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ const AsyncCreatable = React.createClass({
creatableProps.onInputChange(input);
return asyncProps.onInputChange(input);
}}
ref={(ref) => {
creatableProps.ref(ref);
asyncProps.ref(ref);
}}
/>
)}
</Select.Creatable>
Expand Down
Loading

0 comments on commit ed8d087

Please sign in to comment.