├── .eslintrc ├── .github └── issue_template.md ├── .gitignore ├── CONTRIBUTORS.md ├── LICENSE ├── README.md ├── index.d.ts ├── index.js ├── lib ├── helpers │ └── nodeTypes.js ├── react-native-multi-select.js └── styles.js ├── package.json └── yarn.lock /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "es6": true, 5 | "node": true, 6 | "browser": true 7 | }, 8 | "parser": "babel-eslint", 9 | "plugins": ["react","jsx-a11y", "import", "react-native"], 10 | "rules": { 11 | "react/jsx-filename-extension": ["error", { "extensions": [".js", ".jsx"] }], 12 | "react/require-default-props": "off", 13 | "no-trailing-spaces": "off", 14 | "no-useless-escape": "off", 15 | "react/forbid-prop-types": 0, 16 | "no-underscore-dangle": "off", 17 | "comma-dangle": "off", 18 | "quotes": "off", 19 | "arrow-parens": 0, 20 | "jsx-one-expression-per-line": 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ## Issue summary 2 | Add a short description of issue, preferably, a sentence. 3 | 4 | 5 | ### Library versions 6 | 7 | 13 | 14 | 15 | ### Steps to Reproduce 16 | 17 | (Write your steps here:) 18 | 19 | 1. 20 | 2. 21 | 3. 22 | 23 | ### Expected Behavior 24 | 25 | 30 | 31 | (Write what you thought would happen.) 32 | 33 | ### Actual Behavior 34 | 35 | 41 | 42 | (Write what happened. Add screenshots!) 43 | 44 | ### Reproducible Code 45 | 46 | (Paste exact code snippet and instructions to reproduce the issue.) 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Compiled binary addons (http://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Dependency directories 34 | node_modules 35 | jspm_packages 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | # Android/IJ 44 | # 45 | *.iml 46 | **/*.idea 47 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | [enieber](https://github.com/enieber)[arslbbt](https://github.com/arslbbt)[ziyafenn](https://github.com/ziyafenn)[SushilShrestha](https://github.com/SushilShrestha)[remeryAGS](https://github.com/remeryAGS)[MartinCamen](https://github.com/MartinCamen) 2 | 3 | [mikaello](https://github.com/mikaello)[pwoltman](https://github.com/pwoltman)[easyhrworld](https://github.com/easyhrworld)[creedmangrum](https://github.com/creedmangrum)[donedgardo](https://github.com/donedgardo)[toystars](https://github.com/toystars) 4 | 5 | [augustoalegon](https://github.com/augustoalegon) 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Mustapha Babatunde 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-multiple-select 2 | 3 | [![npm](https://img.shields.io/npm/v/react-native-multiple-select.svg)](https://www.npmjs.com/package/react-native-multiple-select) [![Downloads](https://img.shields.io/npm/dt/react-native-multiple-select.svg)](https://www.npmjs.com/package/react-native-multiple-select) [![Licence](https://img.shields.io/npm/l/react-native-multiple-select.svg)](https://www.npmjs.com/package/react-native-multiple-select) 4 | 5 | > Simple multi-select component for react-native (Select2 for react-native). 6 | 7 | ![multiple](https://user-images.githubusercontent.com/16062709/30819847-0907dd1e-a218-11e7-9980-e70b2d8e7953.gif) ![single](https://user-images.githubusercontent.com/16062709/30819849-095d6144-a218-11e7-85b9-4e2b96f9ead9.gif) 8 | 9 | 10 | ## Important notice 11 | I've been super busy with work and other projects lately that I really don't have enough time to dedicate to this project. If you would like to maintain this project, you can drop me an [email](mailto:toystars2008@gmail.com). Thanks. 12 | 13 | ## Installation 14 | 15 | ``` bash 16 | $ npm install react-native-multiple-select --save 17 | ``` 18 | or use yarn 19 | 20 | ``` bash 21 | $ yarn add react-native-multiple-select 22 | ``` 23 | 24 | 25 | ## Usage 26 | Note: Ensure to add and configure [react-native-vector-icons](https://github.com/oblador/react-native-vector-icons) to your project before using this package. 27 | 28 | You can clone and try out the [sample](https://github.com/toystars/RN-multiple-select-sample) app or you can try [sample](https://github.com/AugustoAleGon/react-native-multiple-select-sample) 29 | 30 | The snippet below shows how the component can be used 31 | 32 | 33 | ```javascript 34 | // import component 35 | import React, { Component } from 'react'; 36 | import { View } from 'react-native'; 37 | import MultiSelect from 'react-native-multiple-select'; 38 | 39 | const items = [{ 40 | id: '92iijs7yta', 41 | name: 'Ondo' 42 | }, { 43 | id: 'a0s0a8ssbsd', 44 | name: 'Ogun' 45 | }, { 46 | id: '16hbajsabsd', 47 | name: 'Calabar' 48 | }, { 49 | id: 'nahs75a5sg', 50 | name: 'Lagos' 51 | }, { 52 | id: '667atsas', 53 | name: 'Maiduguri' 54 | }, { 55 | id: 'hsyasajs', 56 | name: 'Anambra' 57 | }, { 58 | id: 'djsjudksjd', 59 | name: 'Benue' 60 | }, { 61 | id: 'sdhyaysdj', 62 | name: 'Kaduna' 63 | }, { 64 | id: 'suudydjsjd', 65 | name: 'Abuja' 66 | } 67 | ]; 68 | 69 | class MultiSelectExample extends Component { 70 | 71 | state = { 72 | selectedItems : [] 73 | }; 74 | 75 | 76 | onSelectedItemsChange = selectedItems => { 77 | this.setState({ selectedItems }); 78 | }; 79 | 80 | render() { 81 | const { selectedItems } = this.state; 82 | 83 | return ( 84 | 85 | { this.multiSelect = component }} 90 | onSelectedItemsChange={this.onSelectedItemsChange} 91 | selectedItems={selectedItems} 92 | selectText="Pick Items" 93 | searchInputPlaceholderText="Search Items..." 94 | onChangeInput={ (text)=> console.log(text)} 95 | altFontFamily="ProximaNova-Light" 96 | tagRemoveIconColor="#CCC" 97 | tagBorderColor="#CCC" 98 | tagTextColor="#CCC" 99 | selectedItemTextColor="#CCC" 100 | selectedItemIconColor="#CCC" 101 | itemTextColor="#000" 102 | displayKey="name" 103 | searchInputStyle={{ color: '#CCC' }} 104 | submitButtonColor="#CCC" 105 | submitButtonText="Submit" 106 | /> 107 | 108 | {this.multiSelect.getSelectedItemsExt(selectedItems)} 109 | 110 | 111 | ); 112 | } 113 | } 114 | 115 | ``` 116 | 117 | The component takes 3 compulsory props - `items`, `uniqueKey` and `onSelectedItemsChange`. Other props are optional. The table below explains more. 118 | 119 | 120 | ## Props 121 | 122 | | Prop | Required | Purpose | 123 | | ------------- |-------------| -----| 124 | | altFontFamily | No | (String) Font family for `searchInputPlaceholderText` | 125 | | canAddItems | No | (Boolean) Defaults to "false". This allows a user to add items to the list of items provided. You need to handle adding the new items in the onAddItem function prop. Items may be added with the return key on the native keyboard. | 126 | | displayKey | No | (String) Defaults to "name". This string will be used to select the key to display the objects in the items array | 127 | | fixedHeight | No | (Boolean) Defaults to false. Specifies if select dropdown take height of content or a fixed height with a scrollBar (There is an issue with this behavior when component is nested in a ScrollView in which scroll event will only be dispatched to parent ScrollView and select component won't be scrollable). See [this issue](https://github.com/toystars/react-native-multiple-select/issues/12) for more info. | 128 | | filterMethod | No | (String) Defaults to "partial". options: ["partial", "full"] Choose the logic on how the system filters items based on searchTerm. partial: checks all individual words and if at least one word matches will include that item. full: checks to ensure the item contains the full substring of searchterm in order minus any leading or trailing spaces. 129 | | flatListProps | No | (Object) Properties for the FlatList. Pass any property that is required on the FlatList of the dropdown menu | 130 | | fontFamily | No | (String) Custom font family to be used in component (affects all text except `searchInputPlaceholderText` described above) | 131 | | fontSize | No | (Number) Font size for selected item name displayed as label for multiselect | 132 | | hideDropdown | No | (Boolean) Defaults false. Hide dropdown menu with a cancel, and use arrow-back | 133 | | hideSubmitButton | No | (Boolean) Defaults to false. Hide submit button from dropdown, and rather use arrow-button in search field | 134 | | hideTags | No | (Boolean) Defaults to false. Hide tokenized selected items, in case selected items are to be shown somewhere else in view (check below for more info) | 135 | | searchIcon | No | (Element, Object, boolean, Function) Element or functional component to change the Search Icon | 136 | | itemFontFamily | No | (String) Font family for each non-selected item in multi-select drop-down | 137 | | itemFontSize | No | (Number) Font size used for each item in the multi-select drop-down | 138 | | itemTextColor | No | (String) Text color for each non-selected item in multi-select drop-down | 139 | | items | Yes | (Array, control prop) List of items to display in the multi-select component. JavaScript Array of objects. Each object must contain a name and unique identifier (Check sample above) | 140 | |noItemsText| No| (String) Text that replace default "no items to display"| 141 | | onAddItem | No | (Function) JavaScript function passed in as an argument. The function is called everytime a new item is added, and receives the entire list of items. Here you should ensure that the new items are added to your provided list of `items` in addition to any other consequences of new items being added. | 142 | | onChangeInput | No | (Function) JavaScript function passed in as an argument. The function is called everytime `TextInput` is changed with the value. | 143 | | onClearSelector | No | (Function) JavaScript function passeed in as an argument. The function is called everytime `back button` is pressed | 144 | | onSelectedItemsChange | Yes | (Function) JavaScript function passed in as an argument. The function is to be defined with an argument (selectedItems). Triggered when `Submit` button is clicked (for multi select) or item is clicked (for single select). (Check sample above) | 145 | | onToggleList | No | (Function) JavaScript function passed in as an argument. The function is called everytime the `multiselect` component is pressed | 146 | | searchInputPlaceholderText | No | (String) Placeholder text displayed in multi-select filter input | 147 | | searchInputStyle | No | (Object) Style object for multi-select input element | 148 | | selectText | No | (String) Text displayed in main component | 149 | | selectedText | No | (String) Text displayed when an item is selected can be replaced by any string| 150 | | selectedItemFontFamily | No | (String) Font family for each selected item in multi-select drop-down | 151 | | selectedItemIconColor | No | (String) Color for `selected` check icon for each selected item in multi-select drop-down | 152 | | selectedItemTextColor | No | (String) Text color for each selected item in multi-select drop-down | 153 | | single | No | (Boolean) Toggles select component between single option and multi option | 154 | | styleDropdownMenu | No | (Style) Style the view of the dropdown menu | 155 | | styleDropdownMenuSubsection | No | (Style) Style the inner view of the dropdown menu | 156 | | styleIndicator | No | (Style) Style the Icon for indicator | 157 | | styleInputGroup | No | (Style) Style the Container of the Text Input Group | 158 | | styleItemsContainer | No | (Style) Style the Container of the items that are displayed in a list | 159 | | styleListContainer | No | (Style) Style the Container of main list. See [this issue] (https://github.com/toystars/react-native-multiple-select/issues/12)| 160 | | styleMainWrapper | No | (Style) Style the Main Container of the MultiSelector | 161 | | styleRowList | No | (Style) Style the Row that is displayed after you | 162 | | styleSelectorContainer | No | (Style) Style the Container of the Selector when user clicks on the dropdown| 163 | | styleTextDropdown | No | (Text Style) Style text of the Dropdown | 164 | | styleTextDropdownSelected | No | (Text Style) Style text of the Dropdown selected | 165 | | styleTextTag | No | (Text Style) Style text of the tag | 166 | | submitButtonColor | No | (String) Background color for submit button | 167 | | submitButtonText | No | (String) Text displayed on submit button | 168 | | tagBorderColor | No | (String) Border color for each selected item | 169 | | tagContainerStyle | No | (Style) Style the container of the tag view | 170 | | tagRemoveIconColor | No | (String) Color to be used for the remove icon in selected items list | 171 | | tagTextColor | No | (String) Text color for selected items list | 172 | | textColor | No | (String) Color for selected item name displayed as label for multiselect | 173 | | textInputProps | No | (Object) Properties for the Text Input. Pass any property that is required on the text input | 174 | | uniqueKey | Yes | (String) Unique identifier that is part of each item's properties. Used internally as means of identifying each item (Check sample below) | 175 | |selectedItems | No | (Array, control prop) List of selected items keys . JavaScript Array of strings, that can be instantiated with the component | 176 | | removeSelected | No | (Boolean) Filter selected items from list to be shown in List | 177 | 178 | ## Note 179 | 180 | - Tokenized selected items can be displayed in any other part of the view by adding a `ref` to the `MultiSelect` component like so `ref={(component) => { this.multiSelect = component }}`. Then add this to any part of the screen you want the tokens to show up: `this.multiSelect.getSelectedItemsExt(selectedItems)`. The `selectedItems` argument passed into the above mentioned method is the same `selectedItems` passed as the main component selected items prop. (See example above). 181 | 182 | - If users shouldn't be able to select any of the items in the dropdown list, set a `disabled` key to true in the item. Such item will be rendered in gray and won't be clickable. 183 | 184 | - When using the `single` prop, `selectedItems` should still be passed in as an array of selected items keys. Also, when an item is selected in the single mode, the selected item is returned as an array of string. 185 | 186 | - The `items` props must be passed as an array of objects with a compulsory `name` key present in each object as the name key is used to display the items in the options component. 187 | 188 | - filterMethod partial example: searchTerm = "University of New" will return "University of New York", "University of New Orleans", "The University of New York" as well as "University of Columbia" and "New England Tech" due to partial matches. 189 | 190 | - filterMethod full example: searchTerm = "University of New" will return" University of New York", "University of New Orleans", "The University of New York" because all three contain the substring "University of New" 191 | 192 | ### Removing all selected items 193 | 194 | To use, add ref to MultiSelect component in parent component, then call method against reference. i.e. 195 | 196 | ```javascript 197 | this._multiSelect = c} 199 | ... 200 | /> 201 | 202 | clearSelectedCategories = () => { 203 | this._multiSelect._removeAllItems(); 204 | }; 205 | 206 | ``` 207 | 208 | 209 | ## Contributing 210 | 211 | Contributions are **welcome** and will be fully **credited**. 212 | 213 | Contributions are accepted via Pull Requests on [Github](https://github.com/toystars/react-native-multiple-select). 214 | 215 | 216 | ### Pull Requests 217 | 218 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 219 | 220 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 221 | 222 | - **Create feature branches** - Don't ask us to pull from your master branch. 223 | 224 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 225 | 226 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 227 | 228 | 229 | ## Issues 230 | 231 | Check issues for current issues. 232 | 233 | ## Contributors 234 | 235 | Here is list of [CONTRIBUTORS](CONTRIBUTORS.md) 236 | 237 | 238 | ## License 239 | 240 | The MIT License (MIT). Please see [LICENSE](LICENSE) for more information. 241 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for react-native-multiple-select 2 | 3 | import * as React from "react"; 4 | import { ViewStyle, TextStyle, TextInputProps, StyleProp, FlatListProps } from 'react-native'; 5 | 6 | export interface MultiSelectProps { 7 | single?: boolean; 8 | selectedItems?: any[]; 9 | items: any[]; 10 | uniqueKey?: string, 11 | tagBorderColor?: string; 12 | tagTextColor?: string; 13 | fontFamily?: string; 14 | tagRemoveIconColor?: string; 15 | onSelectedItemsChange: ((items: any[]) => void), 16 | selectedItemFontFamily?: string; 17 | selectedItemTextColor?: string; 18 | itemFontFamily?: string; 19 | itemTextColor?: string; 20 | itemFontSize?: number; 21 | selectedItemIconColor?: string; 22 | searchIcon?: React.ReactNode; 23 | searchInputPlaceholderText?: string; 24 | searchInputStyle?: StyleProp; 25 | selectText?: string; 26 | styleDropdownMenu?: StyleProp; 27 | styleDropdownMenuSubsection?: StyleProp; 28 | styleIndicator?: StyleProp; 29 | styleInputGroup?: StyleProp; 30 | styleItemsContainer?: StyleProp; 31 | styleListContainer?: StyleProp; 32 | styleMainWrapper?: StyleProp; 33 | styleRowList?: StyleProp; 34 | styleSelectorContainer?: StyleProp; 35 | styleTextDropdown?: StyleProp; 36 | styleTextDropdownSelected?: StyleProp; 37 | altFontFamily?: string; 38 | hideSubmitButton?: boolean; 39 | hideDropdown?: boolean; 40 | submitButtonColor?: string; 41 | submitButtonText?: string; 42 | textColor?: string; 43 | fontSize?: number; 44 | fixedHeight?: boolean; 45 | hideTags?: boolean, 46 | canAddItems?: boolean; 47 | onToggleList?: () => void; 48 | onAddItem?: (newItems: any[]) => void; 49 | onChangeInput?: (text: string) => void; 50 | displayKey?: string; 51 | textInputProps?: TextInputProps; 52 | flatListProps?: FlatListProps; 53 | filterMethod?: string; 54 | noItemsText?: string; 55 | selectedText?: string; 56 | } 57 | 58 | export default class MultiSelect extends React.Component { 59 | getSelectedItemsExt: (items: any[]) => React.ReactNode; 60 | } 61 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * react-native-multi-select 3 | * Copyright(c) 2017 Mustapha Babatunde Oluwaleke 4 | * MIT Licensed 5 | */ 6 | import MultiSelect from './lib/react-native-multi-select'; 7 | 8 | export default MultiSelect; 9 | -------------------------------------------------------------------------------- /lib/helpers/nodeTypes.js: -------------------------------------------------------------------------------- 1 | import PropTypes from 'prop-types'; 2 | 3 | export default PropTypes.oneOfType([ 4 | PropTypes.element, 5 | PropTypes.object, 6 | PropTypes.bool, 7 | PropTypes.func 8 | ]); 9 | -------------------------------------------------------------------------------- /lib/react-native-multi-select.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Text, 4 | View, 5 | TextInput, 6 | TouchableWithoutFeedback, 7 | TouchableOpacity, 8 | FlatList, 9 | UIManager 10 | } from 'react-native'; 11 | import {ViewPropTypes, TextPropTypes} from 'deprecated-react-native-prop-types'; 12 | import PropTypes from 'prop-types'; 13 | import reject from 'lodash/reject'; 14 | import find from 'lodash/find'; 15 | import get from 'lodash/get'; 16 | import Icon from 'react-native-vector-icons/MaterialCommunityIcons'; 17 | 18 | import styles, { colorPack } from './styles'; 19 | import nodeTypes from './helpers/nodeTypes'; 20 | 21 | // set UIManager LayoutAnimationEnabledExperimental 22 | if (UIManager.setLayoutAnimationEnabledExperimental) { 23 | UIManager.setLayoutAnimationEnabledExperimental(true); 24 | } 25 | 26 | const defaultSearchIcon = ( 27 | 33 | ); 34 | 35 | export default class MultiSelect extends Component { 36 | static propTypes = { 37 | single: PropTypes.bool, 38 | selectedItems: PropTypes.array, 39 | items: PropTypes.array.isRequired, 40 | uniqueKey: PropTypes.string, 41 | tagBorderColor: PropTypes.string, 42 | tagTextColor: PropTypes.string, 43 | tagContainerStyle: ViewPropTypes.style, 44 | fontFamily: PropTypes.string, 45 | tagRemoveIconColor: PropTypes.string, 46 | onSelectedItemsChange: PropTypes.func.isRequired, 47 | selectedItemFontFamily: PropTypes.string, 48 | selectedItemTextColor: PropTypes.string, 49 | itemFontFamily: PropTypes.string, 50 | itemTextColor: PropTypes.string, 51 | itemFontSize: PropTypes.number, 52 | selectedItemIconColor: PropTypes.string, 53 | searchIcon: nodeTypes, 54 | searchInputPlaceholderText: PropTypes.string, 55 | searchInputStyle: PropTypes.object, 56 | selectText: PropTypes.string, 57 | styleDropdownMenu: ViewPropTypes.style, 58 | styleDropdownMenuSubsection: ViewPropTypes.style, 59 | styleInputGroup: ViewPropTypes.style, 60 | styleItemsContainer: ViewPropTypes.style, 61 | styleListContainer: ViewPropTypes.style, 62 | styleMainWrapper: ViewPropTypes.style, 63 | styleRowList: ViewPropTypes.style, 64 | styleSelectorContainer: ViewPropTypes.style, 65 | styleTextDropdown: TextPropTypes.style, 66 | styleTextDropdownSelected: TextPropTypes.style, 67 | styleTextTag: TextPropTypes.style, 68 | styleIndicator: ViewPropTypes.style, 69 | altFontFamily: PropTypes.string, 70 | hideSubmitButton: PropTypes.bool, 71 | hideDropdown: PropTypes.bool, 72 | submitButtonColor: PropTypes.string, 73 | submitButtonText: PropTypes.string, 74 | textColor: PropTypes.string, 75 | fontSize: PropTypes.number, 76 | fixedHeight: PropTypes.bool, 77 | hideTags: PropTypes.bool, 78 | canAddItems: PropTypes.bool, 79 | onAddItem: PropTypes.func, 80 | onChangeInput: PropTypes.func, 81 | displayKey: PropTypes.string, 82 | textInputProps: PropTypes.object, 83 | flatListProps: PropTypes.object, 84 | filterMethod: PropTypes.string, 85 | onClearSelector: PropTypes.func, 86 | onToggleList: PropTypes.func, 87 | removeSelected: PropTypes.bool, 88 | noItemsText: PropTypes.string, 89 | selectedText: PropTypes.string 90 | }; 91 | 92 | static defaultProps = { 93 | single: false, 94 | selectedItems: [], 95 | uniqueKey: '_id', 96 | tagBorderColor: colorPack.primary, 97 | tagTextColor: colorPack.primary, 98 | fontFamily: '', 99 | tagRemoveIconColor: colorPack.danger, 100 | selectedItemFontFamily: '', 101 | selectedItemTextColor: colorPack.primary, 102 | searchIcon: defaultSearchIcon, 103 | itemFontFamily: '', 104 | itemTextColor: colorPack.textPrimary, 105 | itemFontSize: 16, 106 | selectedItemIconColor: colorPack.primary, 107 | searchInputPlaceholderText: 'Search', 108 | searchInputStyle: { color: colorPack.textPrimary }, 109 | textColor: colorPack.textPrimary, 110 | selectText: 'Select', 111 | altFontFamily: '', 112 | hideSubmitButton: false, 113 | submitButtonColor: '#CCC', 114 | submitButtonText: 'Submit', 115 | fontSize: 14, 116 | fixedHeight: false, 117 | hideTags: false, 118 | hideDropdown: false, 119 | onChangeInput: () => {}, 120 | displayKey: 'name', 121 | canAddItems: false, 122 | onAddItem: () => {}, 123 | onClearSelector: () => {}, 124 | onToggleList: () => {}, 125 | removeSelected: false, 126 | noItemsText: 'No items to display.', 127 | selectedText: 'selected' 128 | }; 129 | 130 | constructor(props) { 131 | super(props); 132 | this.state = { 133 | selector: false, 134 | searchTerm: '' 135 | }; 136 | } 137 | 138 | shouldComponentUpdate() { 139 | // console.log('Component Updating: ', nextProps.selectedItems); 140 | return true; 141 | } 142 | 143 | getSelectedItemsExt = optionalSelctedItems => ( 144 | 150 | {this._displaySelectedItems(optionalSelctedItems)} 151 | 152 | ); 153 | 154 | _onChangeInput = value => { 155 | const { onChangeInput } = this.props; 156 | if (onChangeInput) { 157 | onChangeInput(value); 158 | } 159 | this.setState({ searchTerm: value }); 160 | }; 161 | 162 | _getSelectLabel = () => { 163 | const { selectText, single, selectedItems, displayKey, selectedText } = this.props; 164 | if (!selectedItems || selectedItems.length === 0) { 165 | return selectText; 166 | } 167 | if (single) { 168 | const item = selectedItems[0]; 169 | const foundItem = this._findItem(item); 170 | return get(foundItem, displayKey) || selectText; 171 | } 172 | return `${selectText} (${selectedItems.length} ${selectedText})`; 173 | }; 174 | 175 | _findItem = itemKey => { 176 | const { items, uniqueKey } = this.props; 177 | return find(items, singleItem => singleItem[uniqueKey] === itemKey) || {}; 178 | }; 179 | 180 | _displaySelectedItems = optionalSelectedItems => { 181 | const { 182 | fontFamily, 183 | tagContainerStyle, 184 | tagRemoveIconColor, 185 | tagBorderColor, 186 | uniqueKey, 187 | tagTextColor, 188 | selectedItems, 189 | displayKey, 190 | styleTextTag 191 | } = this.props; 192 | const actualSelectedItems = optionalSelectedItems || selectedItems; 193 | return actualSelectedItems.map(singleSelectedItem => { 194 | const item = this._findItem(singleSelectedItem); 195 | if (!item[displayKey]) return null; 196 | return ( 197 | 210 | 222 | {item[displayKey]} 223 | 224 | { 226 | this._removeItem(item); 227 | }} 228 | > 229 | 237 | 238 | 239 | ); 240 | }); 241 | }; 242 | 243 | _removeItem = item => { 244 | const { uniqueKey, selectedItems, onSelectedItemsChange } = this.props; 245 | const newItems = reject( 246 | selectedItems, 247 | singleItem => item[uniqueKey] === singleItem 248 | ); 249 | // broadcast new selected items state to parent component 250 | onSelectedItemsChange(newItems); 251 | }; 252 | 253 | _removeAllItems = () => { 254 | const { onSelectedItemsChange } = this.props; 255 | // broadcast new selected items state to parent component 256 | onSelectedItemsChange([]); 257 | }; 258 | 259 | _clearSelector = () => { 260 | this.setState({ 261 | selector: false 262 | }); 263 | }; 264 | 265 | _clearSelectorCallback = () => { 266 | const { onClearSelector } = this.props; 267 | this._clearSelector(); 268 | if (onClearSelector) { 269 | onClearSelector(); 270 | } 271 | }; 272 | 273 | _toggleSelector = () => { 274 | const { onToggleList } = this.props; 275 | this.setState({ 276 | selector: !this.state.selector 277 | }); 278 | if (onToggleList) { 279 | onToggleList(); 280 | } 281 | }; 282 | 283 | _clearSearchTerm = () => { 284 | this.setState({ 285 | searchTerm: '' 286 | }); 287 | }; 288 | 289 | _submitSelection = () => { 290 | this._toggleSelector(); 291 | // reset searchTerm 292 | this._clearSearchTerm(); 293 | }; 294 | 295 | _itemSelected = item => { 296 | const { uniqueKey, selectedItems } = this.props; 297 | return selectedItems.indexOf(item[uniqueKey]) !== -1; 298 | }; 299 | 300 | _addItem = () => { 301 | const { 302 | uniqueKey, 303 | items, 304 | selectedItems, 305 | onSelectedItemsChange, 306 | onAddItem 307 | } = this.props; 308 | let newItems = []; 309 | let newSelectedItems = []; 310 | const newItemName = this.state.searchTerm; 311 | if (newItemName) { 312 | const newItemId = newItemName 313 | .split(' ') 314 | .filter(word => word.length) 315 | .join('-'); 316 | newItems = [...items, { [uniqueKey]: newItemId, name: newItemName }]; 317 | newSelectedItems = [...selectedItems, newItemId]; 318 | onAddItem(newItems); 319 | onSelectedItemsChange(newSelectedItems); 320 | this._clearSearchTerm(); 321 | } 322 | }; 323 | 324 | _toggleItem = item => { 325 | const { 326 | single, 327 | uniqueKey, 328 | selectedItems, 329 | onSelectedItemsChange 330 | } = this.props; 331 | if (single) { 332 | this._submitSelection(); 333 | onSelectedItemsChange([item[uniqueKey]]); 334 | } else { 335 | const status = this._itemSelected(item); 336 | let newItems = []; 337 | if (status) { 338 | newItems = reject( 339 | selectedItems, 340 | singleItem => item[uniqueKey] === singleItem 341 | ); 342 | } else { 343 | newItems = [...selectedItems, item[uniqueKey]]; 344 | } 345 | // broadcast new selected items state to parent component 346 | onSelectedItemsChange(newItems); 347 | } 348 | }; 349 | 350 | _itemStyle = item => { 351 | const { 352 | selectedItemFontFamily, 353 | selectedItemTextColor, 354 | itemFontFamily, 355 | itemTextColor, 356 | itemFontSize 357 | } = this.props; 358 | const isSelected = this._itemSelected(item); 359 | const fontFamily = {}; 360 | if (isSelected && selectedItemFontFamily) { 361 | fontFamily.fontFamily = selectedItemFontFamily; 362 | } else if (!isSelected && itemFontFamily) { 363 | fontFamily.fontFamily = itemFontFamily; 364 | } 365 | const color = isSelected 366 | ? { color: selectedItemTextColor } 367 | : { color: itemTextColor }; 368 | return { 369 | ...fontFamily, 370 | ...color, 371 | fontSize: itemFontSize 372 | }; 373 | }; 374 | 375 | _getRow = item => { 376 | const { selectedItemIconColor, displayKey, styleRowList } = this.props; 377 | return ( 378 | this._toggleItem(item)} 381 | style={[ 382 | styleRowList && styleRowList, 383 | { paddingLeft: 20, paddingRight: 20 } 384 | ]} 385 | > 386 | 387 | 388 | 400 | {item[displayKey]} 401 | 402 | {this._itemSelected(item) ? ( 403 | 410 | ) : null} 411 | 412 | 413 | 414 | ); 415 | }; 416 | 417 | _getRowNew = item => ( 418 | this._addItem(item)} 421 | style={{ paddingLeft: 20, paddingRight: 20 }} 422 | > 423 | 424 | 425 | 437 | Add {item.name} (tap or press return) 438 | 439 | 440 | 441 | 442 | ); 443 | 444 | _filterItems = searchTerm => { 445 | switch (this.props.filterMethod) { 446 | case 'full': 447 | return this._filterItemsFull(searchTerm); 448 | default: 449 | return this._filterItemsPartial(searchTerm); 450 | } 451 | }; 452 | 453 | _filterItemsPartial = searchTerm => { 454 | const { items, displayKey } = this.props; 455 | const filteredItems = []; 456 | items.forEach(item => { 457 | const parts = searchTerm.trim().split(/[ \-:]+/); 458 | const regex = new RegExp(`(${parts.join('|')})`, 'ig'); 459 | if (regex.test(get(item, displayKey))) { 460 | filteredItems.push(item); 461 | } 462 | }); 463 | return filteredItems; 464 | }; 465 | 466 | _filterItemsFull = searchTerm => { 467 | const { items, displayKey } = this.props; 468 | const filteredItems = []; 469 | items.forEach(item => { 470 | if ( 471 | item[displayKey] 472 | .toLowerCase() 473 | .indexOf(searchTerm.trim().toLowerCase()) >= 0 474 | ) { 475 | filteredItems.push(item); 476 | } 477 | }); 478 | return filteredItems; 479 | }; 480 | 481 | _renderItems = () => { 482 | const { 483 | canAddItems, 484 | items, 485 | fontFamily, 486 | uniqueKey, 487 | selectedItems, 488 | flatListProps, 489 | styleListContainer, 490 | removeSelected, 491 | noItemsText 492 | } = this.props; 493 | const { searchTerm } = this.state; 494 | let component = null; 495 | // If searchTerm matches an item in the list, we should not add a new 496 | // element to the list. 497 | let searchTermMatch; 498 | let itemList; 499 | let addItemRow; 500 | let renderItems = searchTerm ? this._filterItems(searchTerm) : items; 501 | // Filtering already selected items 502 | if (removeSelected) { 503 | renderItems = renderItems.filter( 504 | item => !selectedItems.includes(item[uniqueKey]) 505 | ); 506 | } 507 | if (renderItems.length) { 508 | itemList = ( 509 | index.toString()} 513 | listKey={item => item[uniqueKey]} 514 | renderItem={rowData => this._getRow(rowData.item)} 515 | {...flatListProps} 516 | nestedScrollEnabled 517 | /> 518 | ); 519 | searchTermMatch = renderItems.filter(item => item.name === searchTerm) 520 | .length; 521 | } else if (!canAddItems) { 522 | itemList = ( 523 | 524 | 535 | {noItemsText} 536 | 537 | 538 | ); 539 | } 540 | 541 | if (canAddItems && !searchTermMatch && searchTerm.length) { 542 | addItemRow = this._getRowNew({ name: searchTerm }); 543 | } 544 | component = ( 545 | 546 | {itemList} 547 | {addItemRow} 548 | 549 | ); 550 | return component; 551 | }; 552 | 553 | render() { 554 | const { 555 | selectedItems, 556 | single, 557 | fontFamily, 558 | altFontFamily, 559 | searchInputPlaceholderText, 560 | searchInputStyle, 561 | styleDropdownMenu, 562 | styleDropdownMenuSubsection, 563 | hideSubmitButton, 564 | hideDropdown, 565 | submitButtonColor, 566 | submitButtonText, 567 | fontSize, 568 | textColor, 569 | fixedHeight, 570 | hideTags, 571 | textInputProps, 572 | styleMainWrapper, 573 | styleInputGroup, 574 | styleItemsContainer, 575 | styleSelectorContainer, 576 | styleTextDropdown, 577 | styleTextDropdownSelected, 578 | searchIcon, 579 | styleIndicator, 580 | } = this.props; 581 | const { searchTerm, selector } = this.state; 582 | return ( 583 | 592 | {selector ? ( 593 | 599 | 602 | {searchIcon} 603 | 614 | {hideSubmitButton && ( 615 | 616 | 624 | 625 | )} 626 | {!hideDropdown && ( 627 | 634 | )} 635 | 636 | 642 | 643 | {this._renderItems()} 644 | 645 | {!single && !hideSubmitButton && ( 646 | this._submitSelection()} 648 | style={[ 649 | styles.button, 650 | { backgroundColor: submitButtonColor } 651 | ]} 652 | > 653 | 659 | {submitButtonText} 660 | 661 | 662 | )} 663 | 664 | 665 | ) : ( 666 | 667 | 673 | 680 | 681 | 688 | 718 | {this._getSelectLabel()} 719 | 720 | 727 | 728 | 729 | 730 | 731 | {!single && !hideTags && selectedItems.length ? ( 732 | 738 | {this._displaySelectedItems()} 739 | 740 | ) : null} 741 | 742 | )} 743 | 744 | ); 745 | } 746 | } 747 | -------------------------------------------------------------------------------- /lib/styles.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * react-native-multi-select 3 | * Copyright(c) 2017 Mustapha Babatunde Oluwaleke 4 | * MIT Licensed 5 | */ 6 | 7 | export const colorPack = { 8 | primary: '#00A5FF', 9 | primaryDark: '#215191', 10 | light: '#FFF', 11 | textPrimary: '#525966', 12 | placeholderTextColor: '#A9A9A9', 13 | danger: '#C62828', 14 | borderColor: '#e9e9e9', 15 | backgroundColor: '#b1b1b1', 16 | }; 17 | 18 | export default { 19 | footerWrapper: { 20 | flexWrap: 'wrap', 21 | alignItems: 'flex-start', 22 | flexDirection: 'row', 23 | }, 24 | footerWrapperNC: { 25 | width: 320, 26 | flexDirection: 'column', 27 | }, 28 | subSection: { 29 | backgroundColor: colorPack.light, 30 | borderBottomWidth: 1, 31 | borderColor: colorPack.borderColor, 32 | paddingLeft: 0, 33 | paddingRight: 20, 34 | flex: 1, 35 | flexDirection: 'row', 36 | alignItems: 'center', 37 | }, 38 | greyButton: { 39 | height: 40, 40 | borderRadius: 5, 41 | elevation: 0, 42 | backgroundColor: colorPack.backgroundColor, 43 | }, 44 | indicator: { 45 | fontSize: 30, 46 | color: colorPack.placeholderTextColor, 47 | }, 48 | selectedItem: { 49 | flexDirection: 'row', 50 | alignItems: 'center', 51 | paddingLeft: 15, 52 | paddingTop: 3, 53 | paddingRight: 3, 54 | paddingBottom: 3, 55 | margin: 3, 56 | borderRadius: 20, 57 | borderWidth: 2, 58 | }, 59 | button: { 60 | height: 40, 61 | flexDirection: 'row', 62 | justifyContent: 'center', 63 | alignItems: 'center', 64 | }, 65 | buttonText: { 66 | color: colorPack.light, 67 | fontSize: 14, 68 | }, 69 | selectorView: (fixedHeight) => { 70 | const style = { 71 | flexDirection: 'column', 72 | marginBottom: 10, 73 | elevation: 2, 74 | }; 75 | if (fixedHeight) { 76 | style.height = 250; 77 | } 78 | return style; 79 | }, 80 | inputGroup: { 81 | flexDirection: 'row', 82 | alignItems: 'center', 83 | paddingLeft: 16, 84 | backgroundColor: colorPack.light, 85 | }, 86 | dropdownView: { 87 | flexDirection: 'row', 88 | alignItems: 'center', 89 | height: 40, 90 | marginBottom: 10, 91 | }, 92 | }; 93 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-multiple-select", 3 | "version": "0.5.12", 4 | "description": "Simple multi-select component for react-native", 5 | "main": "index.js", 6 | "scripts": { 7 | "precommit": "lint-staged", 8 | "test": "jest", 9 | "lint": "eslint lib/", 10 | "publish": "npm publish --access public" 11 | }, 12 | "jest": { 13 | "preset": "jest-react-native" 14 | }, 15 | "lint-staged": { 16 | "*.{js,json,css}": [ 17 | "prettier --single-quote --write", 18 | "git add" 19 | ] 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/toystars/react-native-multiple-select.git" 24 | }, 25 | "keywords": [ 26 | "reactnative", 27 | "multiselect", 28 | "multi-select", 29 | "react-native" 30 | ], 31 | "author": "Mustapha Babatunde Oluwaleke", 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/toystars/react-native-multiple-select/issues" 35 | }, 36 | "homepage": "https://github.com/toystars/react-native-multiple-select#readme", 37 | "dependencies": { 38 | "prop-types": "^15.7.2" 39 | }, 40 | "devDependencies": { 41 | "babel-eslint": "^10.0.2", 42 | "babel-jest": "24.9.0", 43 | "babel-preset-react-native": "^4.0.1", 44 | "eslint": "^6.5.1", 45 | "eslint-config-airbnb": "^17.1.1", 46 | "eslint-config-react-native": "^4.0.0", 47 | "eslint-plugin-import": "^2.18.1", 48 | "eslint-plugin-jsx-a11y": "^6.2.3", 49 | "eslint-plugin-react": "^7.14.3", 50 | "eslint-plugin-react-native": "^3.7.0", 51 | "husky": "^3.1.0", 52 | "jest": "^24.9.0", 53 | "lint-staged": "^9.4.3", 54 | "prettier": "^1.19.1", 55 | "react-test-renderer": "16.9.0", 56 | "remote-redux-devtools": "^0.5.16" 57 | }, 58 | "peerDependencies": { 59 | "deprecated-react-native-prop-types": ">2.0.0", 60 | "lodash": ">4.17.00", 61 | "react": ">16.6.0", 62 | "react-native": ">0.57.0", 63 | "react-native-vector-icons": ">6.0.0" 64 | } 65 | } 66 | --------------------------------------------------------------------------------