├── .babelrc ├── .codeclimate.yml ├── .eslintrc.js ├── .gitignore ├── .hooks └── pre-commit ├── .prettierrc.js ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── demo.gif ├── dist-modules ├── DragAndDropHelper.js ├── README.md ├── ReactTags.js ├── RemoveComponent.js ├── Suggestions.js ├── Tag.js ├── constants.js └── utils.js ├── dist ├── README.md ├── ReactTags.min.js └── ReactTags.min.js.map ├── docs ├── inline-false.png └── inline-true.png ├── example ├── index.html ├── main.js ├── reactTags.css └── vendor │ ├── JSXTransformer.js │ └── ReactDND.min.js ├── lib ├── DragAndDropHelper.js ├── ReactTags.js ├── RemoveComponent.js ├── Suggestions.js ├── Tag.js ├── constants.js └── utils.js ├── package-lock.json ├── package.json ├── release.sh ├── set_up_hooks.sh ├── test ├── reactTags.test.js ├── setupTest.js ├── suggestions.test.js ├── tag.test.js └── utils.test.js ├── webpack-dev-server.config.js ├── webpack-production.config.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["env", "stage-0", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | plugins: 3 | eslint: 4 | enabled: true 5 | channel: "eslint-4" 6 | config: 7 | config: .eslintrc.js 8 | exclude_patterns: 9 | - "dist/" 10 | - "dist-modules/" 11 | - "**/node_modules/" 12 | - "**/vendor/" 13 | - "**/docs/" 14 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | "off" or 0 - turn the rule off 3 | "warn" or 1 - turn the rule on as a warning (doesn’t affect exit code) 4 | "error" or 2 - turn the rule on as an error (exit code is 1 when triggered) 5 | */ 6 | 7 | module.exports = { 8 | env: { 9 | es6: true, 10 | node: true, 11 | browser: true 12 | }, 13 | extends: ["eslint:recommended", "plugin:react/recommended"], 14 | parserOptions: { 15 | ecmaVersion: "6", 16 | ecmaFeatures: { 17 | jsx: true 18 | }, 19 | sourceType: "module" 20 | }, 21 | plugins: ["react", "jsx-a11y"], 22 | "parser": "babel-eslint", 23 | rules: { 24 | "indent": [2, 2], 25 | "linebreak-style": [2, "unix"], 26 | "quotes": [2, "single"], 27 | "semi": [2, "always"], 28 | "eqeqeq": [2, "smart"], 29 | "no-unused-vars": 1, 30 | "no-undef": 1, 31 | "default-case": 1, 32 | "comma-dangle": [2, "always-multiline"], 33 | "no-trailing-spaces": 2, 34 | "no-extra-bind": 1, 35 | "no-useless-escape": 1, 36 | 37 | /** react rules start here **/ 38 | 39 | "react/jsx-no-duplicate-props": 2, 40 | "react/no-deprecated": 1, 41 | "react/no-is-mounted": 2, 42 | "react/no-unknown-property": 1, 43 | "react/prop-types": 2, 44 | "react/no-direct-mutation-state": 2, 45 | "react/jsx-key": 2, 46 | "react/no-children-prop": 1, 47 | "react/display-name": 1, 48 | "react/no-multi-comp": 1, 49 | "react/sort-comp": 1, 50 | "react/no-did-mount-set-state": 1, 51 | "react/no-did-update-set-state": 1, 52 | "react/no-typos": 2, 53 | "react/jsx-equals-spacing": 2, 54 | "react/jsx-no-undef": 2, 55 | "react/jsx-uses-vars": 1, 56 | "react/no-find-dom-node": 1, 57 | "react/no-render-return-value": 1, 58 | 59 | /** jsx-ally rules start here **/ 60 | "jsx-a11y/alt-text": 2, 61 | "jsx-a11y/html-has-lang": 1, 62 | "jsx-a11y/iframe-has-title": 1, 63 | "jsx-a11y/click-events-have-key-events": 1 64 | }, 65 | overrides: [ 66 | { 67 | files: [ 68 | "**/*.test.js" 69 | ], 70 | env: { 71 | jest: true // now **/*.test.js files' env has both es6 *and* jest 72 | }, 73 | // Can't extend in overrides: https://github.com/eslint/eslint/issues/8813 74 | // "extends": ["plugin:jest/recommended"] 75 | plugins: ["jest"], 76 | rules: { 77 | "jest/no-disabled-tests": "warn", 78 | "jest/no-focused-tests": "error", 79 | "jest/no-identical-title": "error", 80 | "jest/prefer-to-have-length": "warn" 81 | } 82 | } 83 | ], 84 | }; 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .idea 4 | .vscode 5 | coverage 6 | -------------------------------------------------------------------------------- /.hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | printf "\nValidating Javascript files:\n" 3 | 4 | # Check for eslint 5 | which eslint &> /dev/null 6 | if [[ $? -eq 1 ]]; then 7 | printf "\t\033[41mPlease install Eslint\033[0m (npm i --save-dev eslint)" 8 | exit 1 9 | fi 10 | 11 | PASSED=TRUE 12 | for file in $(git diff --cached --name-only | grep -E '\.(js|jsx)$') 13 | 14 | do 15 | git show ":$file" | node_modules/.bin/eslint --stdin --stdin-filename "$file" # we only want to lint the staged changes, not any un-staged changes 16 | if [[ $? -eq 0 ]]; then 17 | printf "\t\033[32mEsLint Passed: $file\033[0m" 18 | else 19 | printf "\t\033[41mEsLint Failed: $FILE\033[0m" 20 | PASSED=false 21 | fi 22 | done 23 | 24 | if ! $PASSED; then 25 | printf "\033[41mCOMMIT FAILED:\033[0m Please fix the eslint errors in your commit" 26 | exit 1 27 | else 28 | printf "\033[42mCOMMIT SUCCEEDED\033[0m\n" 29 | fi 30 | 31 | exit $? 32 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | printWidth: 80, 4 | proseWrap: 'always', 5 | tabWidth: 2, 6 | useTabs: false, 7 | trailingComma: 'es5', 8 | jsxBracketSameLine: true, 9 | arrowParens: 'always', 10 | parser: 'flow', 11 | }; 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "9" 4 | - "8" 5 | - "7" 6 | - "6" 7 | before_install: 8 | - npm install -g npm 9 | - npm install -g greenkeeper-lockfile 10 | install: npm install 11 | before_script: greenkeeper-lockfile-update 12 | # Only the node version 6 job will upload the lockfile 13 | after_script: greenkeeper-lockfile-upload 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing 2 | 3 | 1. Have an idea to change the API - add new props, classnames, etc? Please raise an issue and discuss it before you start implementing it. 4 | 2. Want to submit a new pull request (PR)? Great! Make sure you add tests (or give an explanation why you won't be adding any). Try to get feedback early and often! 5 | 3. This project uses [prettier](https://github.com/prettier/prettier) to enforce a JS styleguide. Make sure you run `npm run format` before sending a PR. 6 | 4. **DO NOT** commit any code in the `dist` and `dist-modules` directory. Code in these files is auto-generated and is on released on Github for easy distribution. Once your PR is merged, one of the owners will release a build with your changes alongside publishing your change on NPM. 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Prakhar Srivastav 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-Tags 2 | === 3 | 4 | [![MIT](https://img.shields.io/npm/l/react-tag-input.svg?style=flat-square)](https://github.com/prakhar1989/react-tags/blob/master/LICENSE) 5 | [![NPM Version](https://img.shields.io/npm/v/react-tag-input.svg?style=flat-square)](https://www.npmjs.com/package/react-tag-input) 6 | [![Dependency Status](https://david-dm.org/yahoo/react-dnd-touch-backend.svg)](https://david-dm.org/yahoo/react-dnd-touch-backend) 7 | [![devDependency Status](https://david-dm.org/yahoo/react-dnd-touch-backend/dev-status.svg)](https://david-dm.org/yahoo/react-dnd-touch-backend#info=devDependencies) 8 | [![npm downloads](https://img.shields.io/npm/dm/react-tag-input.svg?style=flat-square)](https://www.npmjs.com/package/react-tag-input) 9 | [![build status](https://img.shields.io/travis/prakhar1989/react-tags.svg?style=flat-square)](https://travis-ci.org/prakhar1989/react-tags) 10 | [![Greenkeeper badge](https://badges.greenkeeper.io/prakhar1989/react-tags.svg)](https://greenkeeper.io/) 11 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 12 | 13 | 14 | 15 | React-tags is a simple tagging component ready to drop in your React projects. The component is inspired by GMail's *To* field in the compose window. 16 | 17 | ### [Looking for Maintainers](https://github.com/prakhar1989/react-tags/issues/197) 18 | Interested in working on this library as an owner and working with the community to add more features? Let me [know!](https://github.com/prakhar1989/react-tags/issues/197) 19 | 20 | ### Features 21 | - Autocomplete based on a suggestion list 22 | - Keyboard friendly and mouse support 23 | - Reorder tags using drag and drop 24 | 25 | ### Why 26 | Because I was looking for an excuse to build a standalone component and publish it in the wild? To be honest, I needed a tagging component that provided the above features for my [React-Surveyman](http://github.com/prakhar1989/react-surveyman) project. Since I was unable to find one which met my requirements (and the fact that I generally enjoy re-inventing the wheel) this is what I came up with. 27 | 28 | 29 | ### Demo 30 | ![img](demo.gif) 31 | 32 | Check it out [here](https://stackblitz.com/edit/react-tag-input-1nelrc) 33 | 34 | ### Installation 35 | The preferred way of using the component is via NPM 36 | 37 | ``` 38 | npm install --save react-tag-input 39 | ``` 40 | It is, however, also available to be used separately (`dist/ReactTags.min.js`). If you prefer this method remember to include [ReactDND](https://github.com/gaearon/react-dnd) as a dependancy. Refer to the [example](https://stackblitz.com/edit/react-tag-input) to see how this works. 41 | 42 | ### Usage 43 | 44 | Here's a sample implementation that initializes the component with a list of initial `tags` and `suggestions` list. Apart from this, there are multiple events, handlers for which need to be set. For more details, go through the [API](#Options). 45 | 46 | 47 | ```javascript 48 | import React from 'react'; 49 | import ReactDOM from 'react-dom'; 50 | import { WithContext as ReactTags } from 'react-tag-input'; 51 | 52 | const KeyCodes = { 53 | comma: 188, 54 | enter: 13, 55 | }; 56 | 57 | const delimiters = [KeyCodes.comma, KeyCodes.enter]; 58 | 59 | class App extends React.Component { 60 | constructor(props) { 61 | super(props); 62 | 63 | this.state = { 64 | tags: [ 65 | { id: "Thailand", text: "Thailand" }, 66 | { id: "India", text: "India" } 67 | ], 68 | suggestions: [ 69 | { id: 'USA', text: 'USA' }, 70 | { id: 'Germany', text: 'Germany' }, 71 | { id: 'Austria', text: 'Austria' }, 72 | { id: 'Costa Rica', text: 'Costa Rica' }, 73 | { id: 'Sri Lanka', text: 'Sri Lanka' }, 74 | { id: 'Thailand', text: 'Thailand' } 75 | ] 76 | }; 77 | this.handleDelete = this.handleDelete.bind(this); 78 | this.handleAddition = this.handleAddition.bind(this); 79 | this.handleDrag = this.handleDrag.bind(this); 80 | } 81 | 82 | handleDelete(i) { 83 | const { tags } = this.state; 84 | this.setState({ 85 | tags: tags.filter((tag, index) => index !== i), 86 | }); 87 | } 88 | 89 | handleAddition(tag) { 90 | this.setState(state => ({ tags: [...state.tags, tag] })); 91 | } 92 | 93 | handleDrag(tag, currPos, newPos) { 94 | const tags = [...this.state.tags]; 95 | const newTags = tags.slice(); 96 | 97 | newTags.splice(currPos, 1); 98 | newTags.splice(newPos, 0, tag); 99 | 100 | // re-render 101 | this.setState({ tags: newTags }); 102 | } 103 | 104 | render() { 105 | const { tags, suggestions } = this.state; 106 | return ( 107 |
108 | 114 |
115 | ) 116 | } 117 | }; 118 | 119 | ReactDOM.render(, document.getElementById('app')); 120 | ``` 121 | 122 | **A note about `Contexts`** 123 | One of the dependencies of this component is the [react-dnd](https://github.com/gaearon/react-dnd) library. Since the 1.0 version, the original author has changed the API and requires the application using any draggable components to have a top-level [backend](http://gaearon.github.io/react-dnd/docs-html5-backend.html) context. So if you're using this component in an existing Application that uses React-DND you will already have a backend defined, in which case, you should `require` the component *without* the context. 124 | 125 | ```javascript 126 | const ReactTags = require('react-tag-input').WithOutContext; 127 | ``` 128 | Otherwise, you can simply import along with the backend itself (as shown above). If you have ideas to make this API better, I'd [love to hear](https://github.com/prakhar1989/react-tags/issues/new). 129 | 130 | 131 | ### Options 132 | 133 | Option | Type | Default | Description 134 | --- | --- | --- | --- 135 | |[`tags`](#tagsOption) | `Array` | `[]` | An array of tags that are displayed as pre-selected 136 | |[`suggestions`](#suggestionsOption) | `Array` | `[]` | An array of suggestions that are used as basis for showing suggestions 137 | |[`delimiters`](#delimiters) | `Array` | `[ENTER, TAB]` | Specifies which characters should terminate tags input 138 | |[`placeholder`](#placeholderOption) | `String` | `Add new tag` | The placeholder shown for the input 139 | |[`labelField`](#labelFieldOption) | `String` | `text` | Provide an alternative `label` property for the tags 140 | |[`handleAddition`](#handleAdditionOption) | `Function` | `undefined` | Function called when the user wants to add a tag (required) 141 | |[`handleDelete`](#handleDeleteOption) | `Function` | `undefined` | Function called when the user wants to delete a tag (required) 142 | |[`handleDrag`](#handleDragOption) | `Function` | `undefined` | Function called when the user drags a tag 143 | |[`handleFilterSuggestions`](#handleFilterSuggestions) | `Function` | `undefined` | Function called when filtering suggestions 144 | |[`handleTagClick`](#handleTagClickOption) | `Function` | `undefined` | Function called when the user wants to know which tag was clicked 145 | |[`autofocus`](#autofocus) | `Boolean` | `true` | Boolean value to control whether the text-input should be autofocused on mount 146 | |[`allowDeleteFromEmptyInput`](#allowDeleteFromEmptyInput) | `Boolean` | `true` | Boolean value to control whether tags should be deleted when the 'Delete' key is pressed in an empty Input Box 147 | |[`handleInputChange`](#handleInputChange) | `Function` | `undefined` | Event handler for input onChange 148 | |[`handleInputFocus`](#handleInputFocus) | `Function` | `undefined` | Event handler for input onFocus 149 | |[`handleInputBlur`](#handleInputBlur) | `Function` | `undefined` | Event handler for input onBlur 150 | |[`minQueryLength`](#minQueryLength) | `Number` | `2` | How many characters are needed for suggestions to appear 151 | |[`removeComponent`](#removeComponent) | `Boolean` | `false` | Custom delete/remove tag element 152 | |[`autocomplete`](#autocomplete) | `Boolean`/`Number` | `false` | Ensure the first matching suggestion is automatically converted to a tag when a [delimiter](#delimiters) key is pressed 153 | |[`readOnly`](#readOnly) | `Boolean` | `false` | Read-only mode without the input box and `removeComponent` and drag-n-drop features disabled 154 | |[`name`](#nameOption) | `String` | `undefined` | The `name` attribute added to the input 155 | |[`id`](#idOption) | `String` | `undefined` | The `id` attribute added to the input 156 | |[`maxLength`](#maxLength) | `Number` | `Infinity` | The `maxLength` attribute added to the input 157 | |[`tabIndex`](#tabIndex) | `Number` | `0` | The `tabindex` attribute added to the input 158 | |[`inline`](#inline) | `Boolean` | `true` | Render input field and selected tags in-line 159 | 160 | 161 | ##### tags (optional, defaults to `[]`) 162 | An array of tags that are displayed as pre-selected. Each tag should have an `id` property and a property for the label, which is specified by the [`labelField`](#labelFieldOption). 163 | 164 | ```js 165 | // With default labelField 166 | const tags = [ { id: "1", text: "Apples" } ] 167 | 168 | // With labelField of `name` 169 | const tags = [ { id: "1", name: "Apples" } ] 170 | ``` 171 | 172 | 173 | ##### suggestions (optional, defaults to `[]`) 174 | An array of suggestions that are used as basis for showing suggestions. These objects should follow the same structure as the `tags`. So if the `labelField` is `name`, the following would work: 175 | 176 | ```js 177 | // With labelField of `name` 178 | const suggestions = [ 179 | { id: "1", name: "mango" }, 180 | { id: "2", name: "pineapple" }, 181 | { id: "3", name: "orange" }, 182 | { id: "4", name: "pear" } 183 | ]; 184 | ``` 185 | 186 | 187 | ##### delimiters (optional, defaults to `[ENTER, TAB]`) 188 | Specifies which characters should terminate tags input. An array of character codes. 189 | 190 | ```js 191 | const Keys = { 192 | TAB: 9, 193 | SPACE: 32, 194 | COMMA: 188, 195 | }; 196 | 199 | ``` 200 | 201 | 202 | 203 | ##### placeholder (optional, defaults to `Add new tag`) 204 | The placeholder shown for the input. 205 | 206 | ```js 207 | let placeholder = "Add new country" 208 | ``` 209 | 210 | 211 | ##### labelField (optional, defaults to `text`) 212 | Provide an alternative `label` property for the tags. 213 | 214 | ```jsx 215 | 221 | ``` 222 | 223 | This is useful if your data uses the `text` property for something else. 224 | 225 | 226 | 227 | ##### handleAddition (required) 228 | Function called when the user wants to add a tag (either a click, a tab press or carriage return) 229 | 230 | ```js 231 | function(tag) { 232 | // add the tag to the tag list 233 | } 234 | ``` 235 | 236 | 237 | ##### handleDelete (required) 238 | Function called when the user wants to delete a tag 239 | 240 | ```js 241 | function(i) { 242 | // delete the tag at index i 243 | } 244 | ``` 245 | 246 | 247 | ##### handleDrag (optional) 248 | If you want tags to be draggable, you need to provide this function. 249 | Function called when the user drags a tag. 250 | 251 | ```js 252 | function(tag, currPos, newPos) { 253 | // remove tag from currPos and add in newPos 254 | } 255 | ``` 256 | 257 | 258 | ##### handleFilterSuggestions (optional) 259 | To assert control over the suggestions filter, you may contribute a function that is executed whenever a filtered set 260 | of suggestions is expected. By default, the text input value will be matched against each suggestion, and [those that 261 | **start with** the entered text][default-suggestions-filter-logic] will be included in the filters suggestions list. If you do contribute a custom filter 262 | function, you must return an array of suggestions. Please do not mutate the passed suggestions array. 263 | 264 | For example, if you prefer to override the default filter behavior and instead match any suggestions that contain 265 | the entered text _anywhere_ in the suggestion, your `handleFilterSuggestions` property may look like this: 266 | 267 | ```js 268 | function(textInputValue, possibleSuggestionsArray) { 269 | var lowerCaseQuery = textInputValue.toLowerCase() 270 | 271 | return possibleSuggestionsArray.filter(function(suggestion) { 272 | return suggestion.toLowerCase().includes(lowerCaseQuery) 273 | }) 274 | } 275 | ``` 276 | 277 | Note: The above custom filter uses `String.prototype.includes`, which was added to JavaScript as part of the ECMAScript 7 278 | specification. If you need to support a browser that does not yet include support for this method, you will need to 279 | either refactor the above filter based on the capabilities of your supported browsers, or import a [polyfill for 280 | `String.prototype.includes`][includes-polyfill]. 281 | 282 | 283 | ##### handleTagClick (optional) 284 | Function called when the user wants to know which tag was clicked 285 | 286 | ```js 287 | function(i) { 288 | // use the tag details at index i 289 | } 290 | ``` 291 | 292 | 293 | ##### autofocus (optional, defaults to `true`) 294 | Optional boolean param to control whether the text-input should be autofocused on mount. 295 | 296 | ```jsx 297 | 300 | ``` 301 | 302 | 303 | ##### allowDeleteFromEmptyInput (optional, defaults to `true`) 304 | Optional boolean param to control whether tags should be deleted when the 'Delete' key is pressed in an empty Input Box. 305 | 306 | ```js 307 | 310 | ``` 311 | 312 | 313 | ##### handleInputChange (optional) 314 | Optional event handler for input onChange 315 | 316 | ```js 317 | 320 | ``` 321 | 322 | ##### handleInputFocus (optional) 323 | Optional event handler for input onFocus 324 | 325 | ```js 326 | 329 | ``` 330 | 331 | 332 | ##### handleInputBlur (optional) 333 | Optional event handler for input onBlur 334 | 335 | ```js 336 | 339 | ``` 340 | 341 | 342 | ##### minQueryLength (optional, defaults to `2`) 343 | How many characters are needed for suggestions to appear. 344 | 345 | 346 | ##### removeComponent (optional) 347 | If you'd like to supply your own tag delete/remove element, create a React component and pass it as a property to ReactTags using the `removeComponent` option. By default, a simple anchor link with an "x" text node as its only child is rendered, but if you'd like to, say, replace this with a ` 364 | ) 365 | } 366 | } 367 | ``` 368 | 369 | The "ReactTags__remove" className and `onClick` handler properties can be automatically included on the `