react-native-algolia-dropdown
Version:
Real time dropdown search for Algolia with React Native
236 lines (215 loc) • 8.41 kB
JavaScript
import React, { Component, PropTypes } from 'react'
import { StyleSheet, TextInput, View, Text, Animated, Dimensions, TouchableOpacity, ScrollView } from 'react-native'
import algoliasearch from 'algoliasearch/reactnative'
const { height, width } = Dimensions.get('window')
const SEARCH_INPUT_HEIGHT = 30
function NoResults () {
return (
<View style={styles.noResultsContainer}>
<Text style={styles.noResultsText}>No Results</Text>
</View>
)
}
function validateChildProps (children) {
if(children.isArray){
children.forEach((child) => {
if (typeof child.props.index !== 'string') throw new Error(`AlgoliaDropdown: The child component ${child.type.name} must have an "index" attribute which is a string.`)
if (typeof child.props.title !== 'string') throw new Error(`AlgoliaDropdown: The child component ${child.type.name} must have a "title" attribute which is a string.`)
if (typeof child.props.params !== 'undefined' && Object.prototype.toString.call(child.props.params) !== '[object Object]') throw new Error(`AlgoliaDropdown: The child component ${child.type.name} has a params attribute which isn't an object.`)
})
}else{
if (typeof children.props.index !== 'string') throw new Error(`AlgoliaDropdown: The child component ${children.type.name} must have an "index" attribute which is a string.`)
if (typeof children.props.title !== 'string') throw new Error(`AlgoliaDropdown: The child component ${children.type.name} must have a "title" attribute which is a string.`)
if (typeof children.props.params !== 'undefined' && Object.prototype.toString.call(children.props.params) !== '[object Object]') throw new Error(`AlgoliaDropdown: The child component ${children.type.name} has a params attribute which isn't an object.`)
}
}
export default class AlgoliaDropdown extends Component {
constructor (props) {
super()
validateChildProps(props.children)
this.client = algoliasearch(props.appID, props.apiKey)
this.state = {
showOverlay: false,
results: [],
cancelWidth: new Animated.Value(0),
cancelOpacity: new Animated.Value(0),
resultsHeight: new Animated.Value(0),
}
this.handleTextChange = this.handleTextChange.bind(this)
this.handleSearch = this.handleSearch.bind(this)
this.formatQuery = this.formatQuery.bind(this)
this.handleFocus = this.handleFocus.bind(this)
this.handleRemoveFocus = this.handleRemoveFocus.bind(this)
this.getInputStyle = this.getInputStyle.bind(this)
}
handleSearch (err, content) {
if (err) return console.log('Oops', err)
if (content.results.every((result) => result.nbHits === 0)) {
return this.setState({
results: [
this.props.noResultsWrapper
? React.cloneElement(this.props.noResultsWrapper, {key: 'NoResults'})
: <NoResults key='NoResults' />
]
})
}
const results = content.results.map((result, index) => {
return (
<View key={result.index}>
{this.props.titleWrapper
? result.hits.length === 0 ? null : React.cloneElement(this.props.titleWrapper, {title: this.props.children.isArray ? this.props.children[index].props.title : this.props.children.props.title})
: result.hits.length === 0 ? null : <View style={styles.defaultTitleContainer}>
<Text style={styles.defaultTitleText}>
{ this.props.children.isArray ? this.props.children[index].props.title : this.props.children.props.title}
</Text>
</View>}
{result.hits.map((hit, i) => React.cloneElement( this.props.children.isArray ? this.props.children[index] : this.props.children, {data: hit, key: i}))}
</View>
)
})
this.setState({results})
}
formatQuery (query) {
if(this.props.children.isArray){
return this.props.children.map((child) => ({
indexName: child.props.index,
params: child.props.params,
query,
}))
}else{
return [{
indexName: this.props.children.props.index,
params: this.props.children.props.params,
query
}]
}
}
handleTextChange (e) {
if (e.nativeEvent.text === '') this.setState({results: []})
else this.client.search(this.formatQuery(e.nativeEvent.text), this.handleSearch)
}
handleFocus () {
Animated.timing(this.state.resultsHeight, {toValue: height - SEARCH_INPUT_HEIGHT, duration: 500}).start()
Animated.sequence([
Animated.timing(this.state.cancelWidth, {toValue: 63, duration: 200}),
Animated.timing(this.state.cancelOpacity, {toValue: 1, duration: 200}),
]).start()
this.setState({showOverlay: true})
}
handleRemoveFocus () {
this.input.blur()
this.setState({showOverlay: false, results: []})
this.input.clear()
Animated.timing(this.state.resultsHeight, {toValue: 0, duration: 500}).start()
Animated.sequence([
Animated.timing(this.state.cancelOpacity, {toValue: 0, duration: 200}),
Animated.timing(this.state.cancelWidth, {toValue: 0, duration: 200}),
]).start()
}
getInputStyle () {
const baseStyles = {
flex: 1,
height: SEARCH_INPUT_HEIGHT,
borderColor: '#E4E4E4',
borderWidth: 1,
borderRadius: 10,
padding: 7,
paddingLeft: 15,
paddingRight: 15,
margin: 5,
backgroundColor: '#F3F3F3',
color: '#4E595D'
}
return this.props.inputStyle
? {...baseStyles, ...this.props.inputStyle}
: baseStyles
}
render () {
return (
<View style={[styles.container, this.props.style]}>
<View style={styles.searchContainer}>
<TextInput
ref={(ref) => this.input = ref}
autoCorrect={false}
underlineColorAndroid='transparent'
style={this.getInputStyle()}
onFocus={this.handleFocus}
onChange={this.handleTextChange}
placeholder={this.props.placeholder} />
{this.props.sideComponent && this.state.showOverlay === false
? React.cloneElement(this.props.sideComponent)
: null}
<TouchableOpacity
style={{width: this.state.cancelWidth}}
onPress={this.handleRemoveFocus}>
<Animated.Text
style={{opacity: this.state.cancelOpacity, color: this.props.cancelButtonColor, fontSize: 17, padding: 2}}>
{this.props.cancelText}
</Animated.Text>
</TouchableOpacity>
</View>
<Animated.View style={{height: this.state.resultsHeight, backgroundColor: this.props.resultsContainerBackgroundColor}}>
{this.state.showOverlay === true
? <ScrollView automaticallyAdjustContentInsets={false} keyboardDismissMode={'on-drag'} keyboardShouldPersistTaps={true}>
{this.state.results}
{this.props.footerHeight ? <View style={{height: this.props.footerHeight}} /> : null}
</ScrollView>
: null}
</Animated.View>
</View>
)
}
}
AlgoliaDropdown.propTypes = {
appID: PropTypes.string.isRequired,
apiKey: PropTypes.string.isRequired,
titleWrapper: PropTypes.element,
cancelButtonColor: PropTypes.string,
inputStyle: PropTypes.object,
resultsContainerBackgroundColor: PropTypes.string,
noResultsWrapper: PropTypes.element,
style: PropTypes.object,
footerHeight: PropTypes.number,
sideComponent: PropTypes.element,
children: React.PropTypes.oneOfType([
React.PropTypes.arrayOf(React.PropTypes.node),
React.PropTypes.node,
]),
}
AlgoliaDropdown.defaultProps = {
placeholder: 'Search',
cancelText: 'Cancel',
cancelButtonColor: '#4E595D',
resultsContainerBackgroundColor: '#fff',
backgroundColor: '#fff',
}
const styles = StyleSheet.create({
container: {
justifyContent: 'flex-start',
alignItems: 'stretch',
},
searchContainer: {
height: SEARCH_INPUT_HEIGHT + 10,
flexDirection: 'row',
alignItems: 'center',
},
defaultTitleContainer: {
justifyContent: 'center',
padding: 12,
backgroundColor: '#F7F9F9',
},
defaultTitleText: {
fontSize: 15,
color: '#929292'
},
noResultsContainer: {
height: 60,
justifyContent: 'center',
borderBottomWidth: 1,
marginLeft: 10,
borderColor: '#E4E4E4',
},
noResultsText: {
color: '#4E595D'
},
})