|
1 |
| -import React from 'react'; |
2 |
| - |
| 1 | +import React, { Component, PropTypes } from 'react'; |
3 | 2 | import Select from './Select';
|
4 | 3 | import stripDiacritics from './utils/stripDiacritics';
|
5 | 4 |
|
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 | +}; |
7 | 24 |
|
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); |
11 | 45 | }
|
12 |
| - return cache ? cache : null; |
13 |
| -} |
14 | 46 |
|
15 |
| -function updateCache (cache, input, data) { |
16 |
| - if (!cache) return; |
17 |
| - cache[input] = data; |
18 |
| -} |
| 47 | + componentDidMount () { |
| 48 | + const { autoload } = this.props; |
19 | 49 |
|
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(''); |
26 | 52 | }
|
27 | 53 | }
|
28 |
| -} |
29 | 54 |
|
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 | + } |
38 | 65 |
|
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 | + } |
76 | 80 | };
|
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 | + ); |
89 | 91 | }
|
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 | + ) { |
108 | 97 | this.setState({
|
109 |
| - isLoading: false, |
110 |
| - options: data && data.options || [], |
| 98 | + isLoading: true |
111 | 99 | });
|
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 |
| - } |
121 | 100 | }
|
122 |
| - if (this.props.ignoreAccents) input = stripDiacritics(input); |
123 |
| - if (this.props.ignoreCase) input = input.toLowerCase(); |
124 | 101 |
|
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); |
128 | 110 | }
|
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(); |
134 | 114 | }
|
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 | + |
144 | 119 | 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; |
158 | 122 |
|
159 | 123 | 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 |
169 | 127 | };
|
170 | 128 |
|
171 |
| - return children(props); |
| 129 | + return children({ |
| 130 | + ...this.props, |
| 131 | + ...props, |
| 132 | + isLoading, |
| 133 | + onInputChange: this._onInputChange |
| 134 | + }); |
172 | 135 | }
|
173 |
| -}); |
| 136 | +} |
| 137 | + |
| 138 | +Async.propTypes = propTypes; |
| 139 | +Async.defaultProps = defaultProps; |
174 | 140 |
|
175 | 141 | function defaultChildren (props) {
|
176 | 142 | return (
|
177 | 143 | <Select {...props} />
|
178 | 144 | );
|
179 | 145 | };
|
180 |
| - |
181 |
| -module.exports = Async; |
0 commit comments