-
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.
Refactored Async. Cache not yet implemented. Pushing for discussion
- Loading branch information
Showing
6 changed files
with
309 additions
and
735 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
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 |
---|---|---|
@@ -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.
Sorry, something went wrong. |
||
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; |
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
Oops, something went wrong.
Was focus removed accidentally here? Or is there another reason?