├── .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 | 
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
--------------------------------------------------------------------------------