Skip to content

Commit ed8d087

Browse files
committed
Refactored Async. Cache not yet implemented. Pushing for discussion
1 parent 740bf26 commit ed8d087

File tree

6 files changed

+309
-735
lines changed

6 files changed

+309
-735
lines changed

CHANGES.md

-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ Multiple values are now submitted in multiple form fields, which results in an a
1919
## New Select.Async Component
2020

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

2524
## Fixes & Other Changes

README.md

-1
Original file line numberDiff line numberDiff line change
@@ -383,7 +383,6 @@ function onInputKeyDown(event) {
383383
placeholder | string\|node | 'Select ...' | field placeholder, displayed when there's no value
384384
scrollMenuIntoView | bool | true | whether the viewport will shift to display the entire menu when engaged
385385
searchable | bool | true | whether to enable searching feature or not
386-
searchingText | string | 'Searching...' | message to display whilst options are loading via asyncOptions, or when `isLoading` is true
387386
searchPromptText | string\|node | 'Type to search' | label to prompt for search input
388387
tabSelectsValue | bool | true | whether to select the currently focused value when the `[tab]` key is pressed
389388
value | any | undefined | initial field value

examples/src/components/GithubUsers.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ const GithubUsers = React.createClass({
3131
});
3232
},
3333
getUsers (input) {
34+
if (!input) {
35+
return Promise.resolve({ options: [] });
36+
}
37+
3438
return fetch(`https://api.github.com/search/users?q=${input}`)
3539
.then((response) => response.json())
3640
.then((json) => {
@@ -53,7 +57,7 @@ const GithubUsers = React.createClass({
5357
return (
5458
<div className="section">
5559
<h3 className="section-heading">{this.props.label}</h3>
56-
<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} />
60+
<AsyncComponent multi={this.state.multi} value={this.state.value} onChange={this.onChange} onValueClick={this.gotoUser} valueKey="id" labelKey="login" loadOptions={this.getUsers} backspaceRemoves={false} />
5761
<div className="checkbox-list">
5862
<label className="checkbox">
5963
<input type="radio" className="checkbox-control" checked={this.state.multi} onChange={this.switchToMulti}/>

src/Async.js

+114-150
Original file line numberDiff line numberDiff line change
@@ -1,181 +1,145 @@
1-
import React from 'react';
2-
1+
import React, { Component, PropTypes } from 'react';
32
import Select from './Select';
43
import stripDiacritics from './utils/stripDiacritics';
54

6-
let requestId = 0;
5+
// @TODO Implement cache
6+
7+
const propTypes = {
8+
autoload: React.PropTypes.bool.isRequired,
9+
children: React.PropTypes.func.isRequired, // Child function responsible for creating the inner Select component; (props: Object): PropTypes.element
10+
ignoreAccents: React.PropTypes.bool, // whether to strip diacritics when filtering (shared with Select)
11+
ignoreCase: React.PropTypes.bool, // whether to perform case-insensitive filtering (shared with Select)
12+
loadingPlaceholder: PropTypes.string.isRequired,
13+
loadOptions: React.PropTypes.func.isRequired,
14+
options: PropTypes.array.isRequired,
15+
placeholder: React.PropTypes.oneOfType([
16+
React.PropTypes.string,
17+
React.PropTypes.node
18+
]),
19+
searchPromptText: React.PropTypes.oneOfType([
20+
React.PropTypes.string,
21+
React.PropTypes.node
22+
]),
23+
};
724

8-
function initCache (cache) {
9-
if (cache && typeof cache !== 'object') {
10-
cache = {};
25+
const defaultProps = {
26+
autoload: true,
27+
children: defaultChildren,
28+
ignoreAccents: true,
29+
ignoreCase: true,
30+
loadingPlaceholder: 'Loading...',
31+
options: [],
32+
searchPromptText: 'Type to search',
33+
};
34+
35+
export default class Async extends Component {
36+
constructor (props, context) {
37+
super(props, context);
38+
39+
this.state = {
40+
isLoading: false,
41+
options: props.options,
42+
};
43+
44+
this._onInputChange = this._onInputChange.bind(this);
1145
}
12-
return cache ? cache : null;
13-
}
1446

15-
function updateCache (cache, input, data) {
16-
if (!cache) return;
17-
cache[input] = data;
18-
}
47+
componentDidMount () {
48+
const { autoload } = this.props;
1949

20-
function getFromCache (cache, input) {
21-
if (!cache) return;
22-
for (let i = input.length; i >= 0; --i) {
23-
let cacheKey = input.slice(0, i);
24-
if (cache[cacheKey] && (input === cacheKey || cache[cacheKey].complete)) {
25-
return cache[cacheKey];
50+
if (autoload) {
51+
this.loadOptions('');
2652
}
2753
}
28-
}
2954

30-
function thenPromise (promise, callback) {
31-
if (!promise || typeof promise.then !== 'function') return;
32-
return promise.then((data) => {
33-
callback(null, data);
34-
}, (err) => {
35-
callback(err);
36-
});
37-
}
55+
componentWillUpdate (nextProps, nextState) {
56+
const propertiesToSync = ['options'];
57+
propertiesToSync.forEach((prop) => {
58+
if (this.props[prop] !== nextProps[prop]) {
59+
this.setState({
60+
[prop]: nextProps[prop]
61+
});
62+
}
63+
});
64+
}
3865

39-
const stringOrNode = React.PropTypes.oneOfType([
40-
React.PropTypes.string,
41-
React.PropTypes.node
42-
]);
43-
44-
const Async = React.createClass({
45-
propTypes: {
46-
cache: React.PropTypes.any, // object to use to cache results, can be null to disable cache
47-
children: React.PropTypes.func, // Child function responsible for creating the inner Select component; (props: Object): PropTypes.element
48-
ignoreAccents: React.PropTypes.bool, // whether to strip diacritics when filtering (shared with Select)
49-
ignoreCase: React.PropTypes.bool, // whether to perform case-insensitive filtering (shared with Select)
50-
isLoading: React.PropTypes.bool, // overrides the isLoading state when set to true
51-
loadOptions: React.PropTypes.func.isRequired, // function to call to load options asynchronously
52-
loadingPlaceholder: React.PropTypes.string, // replaces the placeholder while options are loading
53-
minimumInput: React.PropTypes.number, // the minimum number of characters that trigger loadOptions
54-
noResultsText: stringOrNode, // placeholder displayed when there are no matching search results (shared with Select)
55-
onInputChange: React.PropTypes.func, // onInputChange handler: function (inputValue) {}
56-
placeholder: stringOrNode, // field placeholder, displayed when there's no value (shared with Select)
57-
searchPromptText: stringOrNode, // label to prompt for search input
58-
searchingText: React.PropTypes.string, // message to display while options are loading
59-
},
60-
getDefaultProps () {
61-
return {
62-
cache: true,
63-
ignoreAccents: true,
64-
ignoreCase: true,
65-
loadingPlaceholder: 'Loading...',
66-
minimumInput: 0,
67-
searchingText: 'Searching...',
68-
searchPromptText: 'Type to search',
69-
};
70-
},
71-
getInitialState () {
72-
return {
73-
cache: initCache(this.props.cache),
74-
isLoading: false,
75-
options: [],
66+
loadOptions (inputValue) {
67+
const { loadOptions } = this.props;
68+
69+
const callback = (error, data) => {
70+
if (callback === this._callback) {
71+
this._callback = null;
72+
73+
const options = data && data.options || [];
74+
75+
this.setState({
76+
isLoading: false,
77+
options
78+
});
79+
}
7680
};
77-
},
78-
componentWillMount () {
79-
this._lastInput = '';
80-
},
81-
componentDidMount () {
82-
this.loadOptions('');
83-
},
84-
componentWillReceiveProps (nextProps) {
85-
if (nextProps.cache !== this.props.cache) {
86-
this.setState({
87-
cache: initCache(nextProps.cache),
88-
});
81+
82+
// Ignore all but the most recent request
83+
this._callback = callback;
84+
85+
const promise = loadOptions(inputValue, callback);
86+
if (promise) {
87+
promise.then(
88+
(data) => callback(null, data),
89+
(error) => callback(error)
90+
);
8991
}
90-
},
91-
focus () {
92-
this.select.focus();
93-
},
94-
resetState () {
95-
this._currentRequestId = -1;
96-
this.setState({
97-
isLoading: false,
98-
options: [],
99-
});
100-
},
101-
getResponseHandler (input) {
102-
let _requestId = this._currentRequestId = requestId++;
103-
return (err, data) => {
104-
if (err) throw err;
105-
if (!this.isMounted()) return;
106-
updateCache(this.state.cache, input, data);
107-
if (_requestId !== this._currentRequestId) return;
92+
93+
if (
94+
this._callback &&
95+
!this.state.isLoading
96+
) {
10897
this.setState({
109-
isLoading: false,
110-
options: data && data.options || [],
98+
isLoading: true
11199
});
112-
};
113-
},
114-
loadOptions (input) {
115-
if (this.props.onInputChange) {
116-
let nextState = this.props.onInputChange(input);
117-
// Note: != used deliberately here to catch undefined and null
118-
if (nextState != null) {
119-
input = '' + nextState;
120-
}
121100
}
122-
if (this.props.ignoreAccents) input = stripDiacritics(input);
123-
if (this.props.ignoreCase) input = input.toLowerCase();
124101

125-
this._lastInput = input;
126-
if (input.length < this.props.minimumInput) {
127-
return this.resetState();
102+
return inputValue;
103+
}
104+
105+
_onInputChange (inputValue) {
106+
const { ignoreAccents, ignoreCase } = this.props;
107+
108+
if (ignoreAccents) {
109+
inputValue = stripDiacritics(inputValue);
128110
}
129-
let cacheResult = getFromCache(this.state.cache, input);
130-
if (cacheResult && Array.isArray(cacheResult.options)) {
131-
return this.setState({
132-
options: cacheResult.options,
133-
});
111+
112+
if (ignoreCase) {
113+
inputValue = inputValue.toLowerCase();
134114
}
135-
this.setState({
136-
isLoading: true,
137-
});
138-
let responseHandler = this.getResponseHandler(input);
139-
let inputPromise = thenPromise(this.props.loadOptions(input, responseHandler), responseHandler);
140-
return inputPromise ? inputPromise.then(() => {
141-
return input;
142-
}) : input;
143-
},
115+
116+
return this.loadOptions(inputValue);
117+
}
118+
144119
render () {
145-
let {
146-
children = defaultChildren,
147-
noResultsText,
148-
...restProps
149-
} = this.props;
150-
let { isLoading, options } = this.state;
151-
if (this.props.isLoading) isLoading = true;
152-
let placeholder = isLoading ? this.props.loadingPlaceholder : this.props.placeholder;
153-
if (isLoading) {
154-
noResultsText = this.props.searchingText;
155-
} else if (!options.length && this._lastInput.length < this.props.minimumInput) {
156-
noResultsText = this.props.searchPromptText;
157-
}
120+
const { children, loadingPlaceholder, placeholder, searchPromptText } = this.props;
121+
const { isLoading, options } = this.state;
158122

159123
const props = {
160-
...restProps,
161-
isLoading,
162-
noResultsText,
163-
onInputChange: this.loadOptions,
164-
options,
165-
placeholder,
166-
ref: (ref) => {
167-
this.select = ref;
168-
}
124+
noResultsText: isLoading ? loadingPlaceholder : searchPromptText,
125+
placeholder: isLoading ? loadingPlaceholder : placeholder,
126+
options: isLoading ? [] : options
169127
};
170128

171-
return children(props);
129+
return children({
130+
...this.props,
131+
...props,
132+
isLoading,
133+
onInputChange: this._onInputChange
134+
});
172135
}
173-
});
136+
}
137+
138+
Async.propTypes = propTypes;
139+
Async.defaultProps = defaultProps;
174140

175141
function defaultChildren (props) {
176142
return (
177143
<Select {...props} />
178144
);
179145
};
180-
181-
module.exports = Async;

src/AsyncCreatable.js

-4
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,6 @@ const AsyncCreatable = React.createClass({
1717
creatableProps.onInputChange(input);
1818
return asyncProps.onInputChange(input);
1919
}}
20-
ref={(ref) => {
21-
creatableProps.ref(ref);
22-
asyncProps.ref(ref);
23-
}}
2420
/>
2521
)}
2622
</Select.Creatable>

0 commit comments

Comments
 (0)