├── .gitignore ├── .npmignore ├── examples ├── CustomExample.js └── Example.js ├── index.js ├── package.json ├── readme.md └── tag.gif /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples/ -------------------------------------------------------------------------------- /examples/CustomExample.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | StyleSheet, 4 | Text, 5 | View, 6 | TextInput, 7 | TouchableHighlight 8 | } from "react-native"; 9 | import AutoTags from "react-native-tag-autocomplete"; 10 | 11 | const contacts = [ 12 | { 13 | email: "mrjoeroddy@gmail.com", 14 | fullName: "Joe Roddy" 15 | }, 16 | { 17 | email: "janedoe@aol.com", 18 | fullName: "Jane Doe" 19 | }, 20 | { 21 | email: "john@doe.gov", 22 | fullName: "John Doe" 23 | }, 24 | { 25 | email: "hungrybox@teamliquid.com", 26 | fullName: "Juan Debiedma" 27 | } 28 | ]; 29 | 30 | export default class CustomExample extends React.Component { 31 | state = { 32 | tagsSelected: [], 33 | suggestions: contacts 34 | }; 35 | 36 | customFilterData = query => { 37 | //override suggestion filter, we can search by specific attributes 38 | query = query.toUpperCase(); 39 | let searchResults = this.state.suggestions.filter(s => { 40 | return ( 41 | s.fullName.toUpperCase().includes(query) || 42 | s.email.toUpperCase().includes(query) 43 | ); 44 | }); 45 | return searchResults; 46 | }; 47 | 48 | customRenderTags = tags => { 49 | //override the tags render 50 | return ( 51 | 52 | {this.state.tagsSelected.map((t, i) => { 53 | return ( 54 | this.handleDelete(i)} 58 | > 59 | 60 | {i}) {t.fullName || t.email} 61 | 62 | 63 | ); 64 | })} 65 | 66 | ); 67 | }; 68 | 69 | customRenderSuggestion = suggestion => { 70 | //override suggestion render the drop down 71 | const name = suggestion.fullName; 72 | return ( 73 | 74 | {name.substr(0, name.indexOf(" "))} - {suggestion.email} 75 | 76 | ); 77 | }; 78 | 79 | handleDelete = index => { 80 | //tag deleted, remove from our tags array 81 | let tagsSelected = this.state.tagsSelected; 82 | tagsSelected.splice(index, 1); 83 | this.setState({ tagsSelected }); 84 | }; 85 | 86 | handleAddition = contact => { 87 | //suggestion clicked, push it to our tags array 88 | this.setState({ tagsSelected: this.state.tagsSelected.concat([contact]) }); 89 | }; 90 | 91 | onCustomTagCreated = userInput => { 92 | //user pressed enter, create a new tag from their input 93 | const contact = { 94 | email: userInput, 95 | fullName: null 96 | }; 97 | this.handleAddition(contact); 98 | }; 99 | 100 | render() { 101 | return ( 102 | 103 | 104 | New Message 105 | 106 | 107 | Recipients 108 | 122 | 123 | 124 | Message 125 | 129 | 130 | 131 | ); 132 | } 133 | } 134 | 135 | const styles = StyleSheet.create({ 136 | customTagsContainer: { 137 | flexDirection: "row", 138 | flexWrap: "wrap", 139 | alignItems: "flex-start", 140 | backgroundColor: "#efeaea", 141 | width: 300 142 | }, 143 | customTag: { 144 | backgroundColor: "#9d30a5", 145 | justifyContent: "center", 146 | alignItems: "center", 147 | height: 30, 148 | marginLeft: 5, 149 | marginTop: 5, 150 | borderRadius: 30, 151 | padding: 8 152 | }, 153 | container: { 154 | flex: 1, 155 | backgroundColor: "#fff", 156 | alignItems: "center" 157 | }, 158 | header: { 159 | backgroundColor: "#9d30a5", 160 | height: 80, 161 | alignSelf: "stretch", 162 | justifyContent: "center", 163 | alignItems: "center", 164 | paddingTop: 15, 165 | marginBottom: 10 166 | }, 167 | autocompleteContainer: { 168 | flex: 1, 169 | left: 20, 170 | position: "absolute", 171 | right: 20, 172 | top: 100, 173 | zIndex: 1 174 | }, 175 | label: { 176 | color: "#614b63", 177 | fontWeight: "bold", 178 | marginBottom: 10 179 | }, 180 | messageContainer: { 181 | marginTop: 160, 182 | height: 200, 183 | alignSelf: "stretch", 184 | marginLeft: 20, 185 | marginRight: 20 186 | }, 187 | message: { 188 | backgroundColor: "#efeaea", 189 | height: 200, 190 | textAlignVertical: "top" 191 | } 192 | }); 193 | -------------------------------------------------------------------------------- /examples/Example.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View, TextInput } from 'react-native'; 3 | import AutoTags from 'react-native-tag-autocomplete'; 4 | 5 | export default class Example extends React.Component { 6 | state = { 7 | tagsSelected: [], 8 | suggestions: [{ name: "mrjoeroddy@gmail.com" }, { name: "janedoe@aol.com" }, 9 | { name: "john@doe.gov" }, { name: "hungrybox@teamliquid.com" }] 10 | //If you don't provide renderTags && filterData props, 11 | //suggestions must have a 'name' attribute to be displayed && searched for. 12 | } 13 | 14 | handleDelete = index => { 15 | //tag deleted, remove from our tags array 16 | let tagsSelected = this.state.tagsSelected; 17 | tagsSelected.splice(index, 1); 18 | this.setState({ tagsSelected }); 19 | } 20 | 21 | handleAddition = contact => { 22 | //suggestion clicked, push it to our tags array 23 | this.setState({ tagsSelected: this.state.tagsSelected.concat([contact]) }); 24 | } 25 | 26 | render() { 27 | return ( 28 | 29 | 30 | 31 | New Message 32 | 33 | 34 | 35 | 36 | Recipients 37 | 38 | 45 | 46 | 47 | Message 48 | 50 | 51 | 52 | ); 53 | } 54 | } 55 | 56 | const styles = StyleSheet.create({ 57 | container: { 58 | flex: 1, 59 | backgroundColor: '#fff', 60 | alignItems: 'center', 61 | }, 62 | header: { 63 | backgroundColor: '#9d30a5', 64 | height: 80, 65 | alignSelf: 'stretch', 66 | justifyContent: 'center', 67 | alignItems: 'center', 68 | paddingTop: 15, 69 | marginBottom: 10, 70 | }, 71 | autocompleteContainer: { 72 | flex: 1, 73 | left: 20, 74 | position: 'absolute', 75 | right: 20, 76 | top: 100, 77 | zIndex: 1 78 | }, 79 | label: { 80 | color: "#614b63", fontWeight: 'bold', marginBottom: 10 81 | }, 82 | messageContainer: { 83 | marginTop: 160, 84 | height: 200, 85 | alignSelf: 'stretch', 86 | marginLeft: 20, 87 | marginRight: 20 88 | }, 89 | message: { 90 | backgroundColor: '#efeaea', 91 | height: 200, 92 | textAlignVertical: 'top', 93 | } 94 | }); 95 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { 3 | StyleSheet, 4 | Text, 5 | TouchableHighlight, 6 | TouchableOpacity, 7 | View 8 | } from "react-native"; 9 | import Autocomplete from "react-native-autocomplete-input"; 10 | 11 | export default class AutoTags extends Component { 12 | state = { 13 | query: "" 14 | }; 15 | 16 | renderTags = () => { 17 | if (this.props.renderTags) { 18 | return this.props.renderTags(this.props.tagsSelected); 19 | } 20 | 21 | const tagMargins = this.props.tagsOrientedBelow 22 | ? { marginBottom: 5 } 23 | : { marginTop: 5 }; 24 | 25 | return ( 26 | 27 | {this.props.tagsSelected.map((t, i) => { 28 | return ( 29 | this.props.handleDelete(i)} 33 | > 34 | {t.name} 35 | 36 | ); 37 | })} 38 | 39 | ); 40 | }; 41 | 42 | handleInput = text => { 43 | if (this.submitting) return; 44 | if (this.props.allowBackspace) { 45 | //TODO: on ios, delete last tag on backspace event && empty query 46 | //(impossible on android atm, no listeners for empty backspace) 47 | } 48 | if (this.props.onChangeText) return this.props.onChangeText(text); 49 | if ( 50 | this.props.createTagOnSpace && 51 | this.props.onCustomTagCreated && 52 | text.length > 1 && 53 | text.charAt(text.length - 1) === " " 54 | ) { 55 | this.setState({ query: "" }); 56 | return this.props.onCustomTagCreated(text.trim()); 57 | } else if (this.props.createTagOnSpace && !this.props.onCustomTagCreated) { 58 | console.error( 59 | "When enabling createTagOnSpace, you must provide an onCustomTagCreated function" 60 | ); 61 | } 62 | 63 | if (text.charAt(text.length - 1) === "\n") { 64 | return; // prevent onSubmit bugs 65 | } 66 | 67 | this.setState({ query: text }); 68 | }; 69 | 70 | filterData = query => { 71 | if (!query || query.trim() == "" || !this.props.suggestions) { 72 | return; 73 | } 74 | if (this.props.filterData) { 75 | return this.props.filterData(query); 76 | } 77 | let suggestions = this.props.suggestions; 78 | let results = []; 79 | query = query.toUpperCase(); 80 | suggestions.forEach(i => { 81 | if (i.name.toUpperCase().includes(query)) { 82 | results.push(i); 83 | } 84 | }); 85 | return results; 86 | }; 87 | 88 | onSubmitEditing = () => { 89 | const { query } = this.state; 90 | if (!this.props.onCustomTagCreated || query.trim() === "") return; 91 | this.setState({ query: "" }, () => this.props.onCustomTagCreated(query)); 92 | 93 | // prevents an issue where handleInput() will overwrite 94 | // the query clear in some circumstances 95 | this.submitting = true; 96 | setTimeout(() => { 97 | this.submitting = false; 98 | }, 30); 99 | }; 100 | 101 | addTag = tag => { 102 | this.props.handleAddition(tag); 103 | this.setState({ query: "" }); 104 | }; 105 | 106 | render() { 107 | const { query } = this.state; 108 | const data = this.filterData(query); 109 | 110 | return ( 111 | 112 | {!this.props.tagsOrientedBelow && 113 | this.props.tagsSelected && 114 | this.renderTags()} 115 | this.handleInput(text)} 122 | onSubmitEditing={this.onSubmitEditing} 123 | multiline={true} 124 | autoFocus={this.props.autoFocus === false ? false : true} 125 | renderItem={({ item, i }) => ( 126 | this.addTag(item)}> 127 | {this.props.renderSuggestion ? ( 128 | this.props.renderSuggestion(item) 129 | ) : ( 130 | {item.name} 131 | )} 132 | 133 | )} 134 | inputContainerStyle={ 135 | this.props.inputContainerStyle || styles.inputContainerStyle 136 | } 137 | containerStyle={this.props.containerStyle || styles.containerStyle} 138 | underlineColorAndroid="transparent" 139 | style={{ backgroundColor: "#efeaea" }} 140 | listContainerStyle={{ 141 | backgroundColor: this.props.tagsOrientedBelow 142 | ? "#efeaea" 143 | : "transparent" 144 | }} 145 | {...this.props} 146 | /> 147 | {this.props.tagsOrientedBelow && 148 | this.props.tagsSelected && 149 | this.renderTags()} 150 | 151 | ); 152 | } 153 | } 154 | 155 | const styles = StyleSheet.create({ 156 | AutoTags: { 157 | flexDirection: "row", 158 | flexWrap: "wrap", 159 | alignItems: "flex-start" 160 | }, 161 | tags: { 162 | flexDirection: "row", 163 | flexWrap: "wrap", 164 | alignItems: "flex-start", 165 | backgroundColor: "#efeaea", 166 | width: 300 167 | }, 168 | tag: { 169 | backgroundColor: "rgb(244, 244, 244)", 170 | justifyContent: "center", 171 | alignItems: "center", 172 | height: 30, 173 | marginLeft: 5, 174 | borderRadius: 30, 175 | padding: 8 176 | }, 177 | inputContainerStyle: { 178 | borderRadius: 0, 179 | paddingLeft: 5, 180 | height: 40, 181 | width: 300, 182 | justifyContent: "center", 183 | borderColor: "transparent", 184 | alignItems: "stretch", 185 | backgroundColor: "#efeaea" 186 | }, 187 | containerStyle: { 188 | minWidth: 200, 189 | maxWidth: 300 190 | } 191 | }); 192 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-tag-autocomplete", 3 | "version": "1.0.22", 4 | "description": "Autocompleting tag list for react native.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "mrjoeroddy@gmail.com", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "react": "16.0.0-alpha.12", 13 | "react-native": "^0.46.1" 14 | }, 15 | "dependencies": { 16 | "react-native-autocomplete-input": "^4.1.0" 17 | }, 18 | "peerDependencies": {} 19 | } 20 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # react-native-tag-autocomplete 2 | 3 | Tag autocomplete component for contacts, groups, etc. 4 | 5 | ![Example](https://raw.githubusercontent.com/JoeRoddy/react-native-tag-autocomplete/master/tag.gif) 6 | 7 | ### Up and Running 8 | 9 | ```shell 10 | $ npm install --save react-native-tag-autocomplete 11 | ``` 12 | 13 | ### Example 14 | 15 | ```javascript 16 | //... 17 | import AutoTags from 'react-native-tag-autocomplete'; 18 | // ... 19 | state = { 20 | suggestions : [ {name:'Mickey Mouse'}, ], 21 | tagsSelected : [] 22 | } 23 | 24 | handleDelete = index => { 25 | let tagsSelected = this.state.tagsSelected; 26 | tagsSelected.splice(index, 1); 27 | this.setState({ tagsSelected }); 28 | } 29 | 30 | handleAddition = suggestion => { 31 | this.setState({ tagsSelected: this.state.tagsSelected.concat([suggestion]) }); 32 | } 33 | 34 | render() { 35 | return ( 36 | 42 | ); 43 | } 44 | // ... 45 | ``` 46 | 47 | ### Props 48 | 49 | | Prop | Type | Required | Description | 50 | | :----------------- | :------: | :------: | :----------------------------------------------------------------------------------------------- | 51 | | suggestions | array | yes | Array of suggestion objects. They must have a 'name' prop if not overriding filter && renderTags | 52 | | tagsSelected | array | yes | List of tags that have already been selected | 53 | | handleAddition | function | yes | Handler for when suggestion is selected (normally just push to tagsSelected) | 54 | | handleDelete | function | yes | Handler called with index when tag is clicked | 55 | | placeholder | string | no | Input placeholder | 56 | | renderTags | function | no | Override the render tags and it's styles | 57 | | renderSuggestion | function | no | Override the suggestions dropdown items | 58 | | filterData | function | no | Override the search function, allows you to filter by props other than name | 59 | | onCustomTagCreated | function | no | Function called with user input when user presses enter | 60 | | createTagOnSpace | boolean | no | calls onCustomTagCreated when user presses space | 61 | | tagStyles | object | no | Override the default tag styling | 62 | | tagsOrientedBelow | boolean | no | Move tags below the input instead of above (default). | 63 | 64 | ### Android 65 | 66 | This repository wraps [react-native-autocomplete-input](https://github.com/l-urence/react-native-autocomplete-input), so their limitations will also apply here. 67 | 68 | As such: 69 | 70 | "Android does not support overflows ([#20](https://github.com/l-urence/react-native-autocomplete-input/issues/20)), for that reason it is necessary to wrap the autocomplete into a _absolute_ positioned view on Android. This will allow the suggestion list to overlap other views inside your component." 71 | 72 | ```javascript 73 | //... 74 | 75 | render() { 76 | return( 77 | 78 | 79 | 80 | 81 | 82 | Some content 83 | 84 | 85 | ); 86 | } 87 | 88 | //... 89 | 90 | const styles = StyleSheet.create({ 91 | autocompleteContainer: { 92 | flex: 1, 93 | left: 0, 94 | position: 'absolute', 95 | right: 0, 96 | top: 0, 97 | zIndex: 1 98 | } 99 | }); 100 | ``` 101 | 102 | 103 | ## Pull Requests 104 | 105 | I'm a dummy, so any PR's are wholly appreciated <3. 106 | -------------------------------------------------------------------------------- /tag.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoeRoddy/react-native-tag-autocomplete/91b1831e9a7a6e7d3636044f63abf5ac670543e4/tag.gif --------------------------------------------------------------------------------