# react-native-autocomplete-dropdown

Dropdown Item picker with search and autocomplete (typeahead) functionality for react native

[![license](https://img.shields.io/github/license/onmotion/react-native-autocomplete-dropdown)](https://img.shields.io/github/license/onmotion/react-native-autocomplete-dropdown)
[![npm](https://img.shields.io/npm/v/react-native-autocomplete-dropdown.svg)](https://npmjs.com/package/react-native-autocomplete-dropdown)
[![npm](https://img.shields.io/npm/dm/react-native-autocomplete-dropdown.svg)](https://npmjs.com/package/react-native-autocomplete-dropdown)

<p style="text-align: center;" align="center">
    <img src="./screens/Example.png" width="500px" >
</p>

## Demo

<p style="text-align: center;" align="center">
    <img src="./screens/android.gif" width="280px" >
    <img src="./screens/ios.gif" width="280px" >
</p>

> Run expo snack demo [@onmotion/react-native-autocomplete-dropdown](https://snack.expo.io/@onmotion/react-native-autocomplete-dropdown)

<img src="./screens/expo-qr.png" width="150">

## Nav

- [react-native-autocomplete-dropdown](#react-native-autocomplete-dropdown)
    - [Demo](#demo)
    - [Nav](#nav)
    - [Installation](#installation)
    - [Post-install Steps](#post-install-steps)
        - [iOS](#ios)
        - [Android](#android)
    - [Usage](#usage)
        - [Dataset item format](#dataset-item-format)
        - [Example with local Dataset](#example-with-local-dataset)
        - [Example with remote requested Dataset](#example-with-remote-requested-dataset)
    - [Options](#options)
    - [Troubleshooting](#troubleshooting)
        - [zIndex hell on iOS](#zindex-hell-on-ios)

## Installation

Run: `npm install --save react-native-autocomplete-dropdown` or `yarn add react-native-autocomplete-dropdown`

## Post-install Steps

Make sure **react-native-vector-icons** is installed. Follow the guides
<https://github.com/oblador/react-native-vector-icons>

`yarn add react-native-vector-icons`

### iOS

Run: `npx pod-install` for install react-native-vector-iconsreact-native-vector-icons dependency (if not installed yet).

### Android

Follow the guides from <https://github.com/oblador/react-native-vector-icons#android> for install react-native-vector-icons dependency (if not installed yet).

## Usage

import the package

```js
import { AutocompleteDropdown } from 'react-native-autocomplete-dropdown';
```

### Dataset item format

`dataSet` property must be an **array of objects** or **null**. Object required keys are:

```js
{
    id: 'some uniq string id',
    title: 'list item title'
}
```

### Example with local Dataset

```js
const [selectedItem, setSelectedItem] = useState(null);

<AutocompleteDropdown
  clearOnFocus={false}
  closeOnBlur={true}
  closeOnSubmit={false}
  initialValue={{ id: '2' }} // or just '2'
  onSelectItem={setSelectedItem}
  dataSet={[
    { id: '1', title: 'Alpha' },
    { id: '2', title: 'Beta' },
    { id: '3', title: 'Gamma' },
  ]}
/>;
```

### Example with remote requested Dataset

```js
import React, { memo, useCallback, useRef, useState } from 'react'
import { Button, Dimensions, Text, View, Platform } from 'react-native'
import { AutocompleteDropdown } from 'react-native-autocomplete-dropdown'

import Feather from 'react-native-vector-icons/Feather'
Feather.loadFont()

export const RemoteDataSetExample2 = memo(() => {
  const [loading, setLoading] = useState(false)
  const [suggestionsList, setSuggestionsList] = useState(null)
  const [selectedItem, setSelectedItem] = useState(null)
  const dropdownController = useRef(null)

  const searchRef = useRef(null)

  const getSuggestions = useCallback(async q => {
    const filterToken = q.toLowerCase()
    console.log('getSuggestions', q)
    if (typeof q !== 'string' || q.length < 3) {
      setSuggestionsList(null)
      return
    }
    setLoading(true)
    const response = await fetch('https://jsonplaceholder.typicode.com/posts')
    const items = await response.json()
    const suggestions = items
      .filter(item => item.title.toLowerCase().includes(filterToken))
      .map(item => ({
        id: item.id,
        title: item.title,
      }))
    setSuggestionsList(suggestions)
    setLoading(false)
  }, [])

  const onClearPress = useCallback(() => {
    setSuggestionsList(null)
  }, [])

  const onOpenSuggestionsList = useCallback(isOpened => {}, [])

  return (
    <>
      <View
        style={[
          { flex: 1, flexDirection: 'row', alignItems: 'center' },
          Platform.select({ ios: { zIndex: 1 } }),
        ]}>
        <AutocompleteDropdown
          ref={searchRef}
          controller={controller => {
            dropdownController.current = controller
          }}
          // initialValue={'1'}
          direction={Platform.select({ ios: 'down' })}
          dataSet={suggestionsList}
          onChangeText={getSuggestions}
          onSelectItem={item => {
            item && setSelectedItem(item.id)
          }}
          debounce={600}
          suggestionsListMaxHeight={Dimensions.get('window').height * 0.4}
          onClear={onClearPress}
          //  onSubmit={(e) => onSubmitSearch(e.nativeEvent.text)}
          onOpenSuggestionsList={onOpenSuggestionsList}
          loading={loading}
          useFilter={false} // set false to prevent rerender twice
          textInputProps={{
            placeholder: 'Type 3+ letters (dolo...)',
            autoCorrect: false,
            autoCapitalize: 'none',
            style: {
              borderRadius: 25,
              backgroundColor: '#383b42',
              color: '#fff',
              paddingLeft: 18,
            },
          }}
          rightButtonsContainerStyle={{
            right: 8,
            height: 30,

            alignSelf: 'center',
          }}
          inputContainerStyle={{
            backgroundColor: '#383b42',
            borderRadius: 25,
          }}
          suggestionsListContainerStyle={{
            backgroundColor: '#383b42',
          }}
          containerStyle={{ flexGrow: 1, flexShrink: 1 }}
          renderItem={(item, text) => <Text style={{ color: '#fff', padding: 15 }}>{item.title}</Text>}
          ChevronIconComponent={<Feather name="chevron-down" size={20} color="#fff" />}
          ClearIconComponent={<Feather name="x-circle" size={18} color="#fff" />}
          inputHeight={50}
          showChevron={false}
          closeOnBlur={false}
          //  showClear={false}
        />
        <View style={{ width: 10 }} />
        <Button style={{ flexGrow: 0 }} title="Toggle" onPress={() => dropdownController.current.toggle()} />
      </View>
      <Text style={{ color: '#668', fontSize: 13 }}>Selected item id: {JSON.stringify(selectedItem)}</Text>
    </>
  )
})

```

More examples see at <https://github.com/onmotion/react-native-autocomplete-dropdown/tree/main/example>

Run

```bash
cd example
yarn install
yarn add react-native-vector-icons
npx pod-install
npm run ios

```

## Options

| **Option** 	| **Description** 	| **Type** 	| **Default** 	|
|---	|---	|---	|---	|
| `dataSet` 	| set of list items 	| array 	| null 	|
| `initialValue` 	| string (**id**) or object that contain **id** 	| string \| object 	| null 	|
| `loading` 	| loading state 	| bool 	| false 	|
| `useFilter` 	| whether use local filter by dataSet (useful set to false for remote filtering to prevent rerender twice) 	| bool 	| true 	|
| `showClear` 	| show clear button 	| bool 	| true 	|
| `showChevron` 	| show chevron (open/close) button 	| bool 	| true 	|
| `closeOnBlur` 	| whether to close dropdown on blur 	| bool 	| false 	|
| `closeOnSubmit` 	| whether to close dropdown on submit 	| bool 	| false 	|
| `clearOnFocus` 	| whether to clear typed text on focus 	| bool 	| true 	|
| `debounce` 	| wait **ms** before call `onChangeText` 	| number 	| 0 	|
| `suggestionsListMaxHeight` 	| max height of dropdown 	| number 	| 200 	|
| `direction` 	| "up" or "down" 	| string 	| down + auto calculate 	|
| `position` 	| "relative" or "absolute" 	| string 	| relative 	|
| `bottomOffset` 	| for calculate dropdown direction (e.g. tabbar) 	| number 	| 0 	|
| `onChangeText` 	| event textInput onChangeText 	| function 	|  	|
| `onSelectItem` 	| event onSelectItem 	| function 	|  	|
| `onOpenSuggestionsList` 	| event onOpenSuggestionsList 	| function 	|  	|
| `onChevronPress` 	| event onChevronPress 	| function 	|  	|
| `onClear` 	| event on clear button press 	| function 	|  	|
| `onSubmit` 	| event on submit KB button press 	| function 	|  	|
| `onBlur` 	| event fired on text input blur 	| function 	|  	|
| `onFocus` 	| event on focus text input 	| function 	|  	|
| `renderItem` 	| JSX for render item `(item, searchText) => JSX \| null` if return null then the element will not be displayed 	| function 	| item.title 	|
| `controller` 	| return reference to module controller with methods **close, open, toggle, clear, setInputText, setItem** 	| function 	|  	|
| `containerStyle` 	|  	| ViewStyle 	|  	|
| `rightButtonsContainerStyle` 	|  	| ViewStyle 	|  	|
| `suggestionsListContainerStyle` 	|  	| ViewStyle 	|  	|
| `suggestionsListTextStyle` 	|  	| TextStyle 	| styles of suggestions list text items 	|
| `ChevronIconComponent` 	|  	| React.Component 	| Feather chevron icon 	|
| `ClearIconComponent` 	|  	| React.Component 	| Feather x icon 	|
| ~~ScrollViewComponent~~ 	| removed in 2.0.0 based on FlatList 	| React.Component name 	| ScrollView that provide suggestions content 	|
| `EmptyResultComponent` 	| replace the default `` Component on empty result 	| React.Component 	|  	|
| `InputComponent` 	| input element component 	| React.ComponentType 	| TextInput 	|
| `emptyResultText` 	| replace the default "Nothing found" text on empty result 	| string 	| "Nothing found" 	|
| `textInputProps` 	| text input props 	| TextInputProps 	|  	|
| `flatListProps` 	| props for \ component 	| FlatListProps\ 	|  	|

## Troubleshooting

### zIndex hell on iOS

As decribed here <https://docs.expo.dev/ui-programming/z-index/> on iOS for absolute positioned items we must respect the zIndex of the element parent.

So for example if you do smth like

```js
          <View style={[styles.section]}>
            <Text style={styles.sectionTitle}>First</Text>
            <AutocompleteDropdown
              dataSet={[
                { id: '1', title: 'Alpha' },
                { id: '2', title: 'Beta' },
                { id: '3', title: 'Gamma' },
              ]}
            />
          </View>
          <View style={[styles.section]}>
            <Text style={styles.sectionTitle}>Second</Text>
            <AutocompleteDropdown
              dataSet={[
                { id: '1', title: 'Alpha' },
                { id: '2', title: 'Beta' },
                { id: '3', title: 'Gamma' },
              ]}
            />
          </View>
```

you would see
<img width="384" alt="Screenshot 2022-04-24 at 20 59 38" src="https://user-images.githubusercontent.com/12899080/164989990-2b4269e1-406a-48a5-9ec7-25e4f53d2fb0.png">

But if it change with calculated zIndex:

```js
          <View
            style={[styles.section, Platform.select({ ios: { zIndex: 10 } })]}>
            <Text style={styles.sectionTitle}>First</Text>
            <AutocompleteDropdown
              dataSet={[
                { id: '1', title: 'Alpha' },
                { id: '2', title: 'Beta' },
                { id: '3', title: 'Gamma' },
              ]}
            />
          </View>
          <View
            style={[styles.section, Platform.select({ ios: { zIndex: 9 } })]}>
            <Text style={styles.sectionTitle}>Second</Text>
            <AutocompleteDropdown
              dataSet={[
                { id: '1', title: 'Alpha' },
                { id: '2', title: 'Beta' },
                { id: '3', title: 'Gamma' },
              ]}
            />
          </View>
```

it will be rendered as expected
<img width="381" alt="Screenshot 2022-04-24 at 21 03 16" src="https://user-images.githubusercontent.com/12899080/164990163-7d2aa98d-3988-4636-97a3-8be229489472.png">

And the same, if you want render dropdown list to **up** direction, you should switch zIndexes respectively

More info about this behaviour: <https://docs.expo.dev/ui-programming/z-index/>
